mirror of
https://gitee.com/bootx/dax-pay-ui.git
synced 2025-09-11 14:39:20 +00:00
feat: add search page
This commit is contained in:
@@ -1,3 +1,9 @@
|
|||||||
|
## Wip
|
||||||
|
|
||||||
|
### ✨ Features
|
||||||
|
|
||||||
|
- 移除左侧菜单搜索,新增顶部菜单搜索功能
|
||||||
|
|
||||||
## 2.0.0-rc.13 (2020-12-10)
|
## 2.0.0-rc.13 (2020-12-10)
|
||||||
|
|
||||||
## (破坏性更新) Breaking changes
|
## (破坏性更新) Breaking changes
|
||||||
|
@@ -23,7 +23,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconify/iconify": "^2.0.0-rc.2",
|
"@iconify/iconify": "^2.0.0-rc.2",
|
||||||
"@vueuse/core": "^4.0.0-rc.7",
|
"@vueuse/core": "^4.0.0-rc.7",
|
||||||
"ant-design-vue": "^2.0.0-rc.3",
|
"ant-design-vue": "^2.0.0-rc.4",
|
||||||
"apexcharts": "^3.22.3",
|
"apexcharts": "^3.22.3",
|
||||||
"axios": "^0.21.0",
|
"axios": "^0.21.0",
|
||||||
"crypto-es": "^1.2.6",
|
"crypto-es": "^1.2.6",
|
||||||
|
@@ -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 { 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'));
|
||||||
|
|
||||||
|
withInstall(AppLocalePicker, AppLogo, AppProvider, AppSearch);
|
||||||
|
|
||||||
export { useAppProviderContext } from './src/useAppContext';
|
export { useAppProviderContext } from './src/useAppContext';
|
||||||
|
|
||||||
export { AppLocalePicker, AppLogo, AppProvider };
|
|
||||||
|
55
src/components/Application/src/search/AppSearch.vue
Normal file
55
src/components/Application/src/search/AppSearch.vue
Normal file
@@ -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>
|
76
src/components/Application/src/search/AppSearchFooter.vue
Normal file
76
src/components/Application/src/search/AppSearchFooter.vue
Normal file
@@ -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>
|
198
src/components/Application/src/search/AppSearchModal.vue
Normal file
198
src/components/Application/src/search/AppSearchModal.vue
Normal file
@@ -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>
|
173
src/components/Application/src/search/useMenuSearch.ts
Normal file
173
src/components/Application/src/search/useMenuSearch.ts
Normal file
@@ -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 { withInstall } from '../util';
|
||||||
|
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||||
|
|
||||||
|
export const Authority = createAsyncComponent(() => import('./src/index.vue'));
|
||||||
|
|
||||||
withInstall(Authority);
|
withInstall(Authority);
|
||||||
|
|
||||||
export { 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 { withInstall } from '../util';
|
||||||
|
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||||
|
|
||||||
|
export const BasicArrow = createAsyncComponent(() => import('./src/BasicArrow.vue'));
|
||||||
|
export const BasicHelp = createAsyncComponent(() => import('./src/BasicHelp.vue'));
|
||||||
|
export const BasicTitle = createAsyncComponent(() => import('./src/BasicTitle.vue'));
|
||||||
|
|
||||||
withInstall(BasicArrow, BasicHelp, BasicTitle);
|
withInstall(BasicArrow, BasicHelp, BasicTitle);
|
||||||
|
|
||||||
export { BasicArrow, BasicHelp, BasicTitle };
|
|
||||||
|
@@ -31,18 +31,33 @@
|
|||||||
// Speed: 1x
|
// Speed: 1x
|
||||||
.fade-bottom-enter-active,
|
.fade-bottom-enter-active,
|
||||||
.fade-bottom-leave-active {
|
.fade-bottom-leave-active {
|
||||||
transition: opacity 0.2s, transform 0.25s;
|
transition: opacity 0.25s, transform 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-bottom-enter-from,
|
.fade-bottom-enter-from {
|
||||||
.fade-bottom-enter {
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-8%);
|
transform: translateY(-10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-bottom-leave-to {
|
.fade-bottom-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(8%);
|
transform: translateY(10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
// fade-scale
|
||||||
|
.fade-scale-leave-active,
|
||||||
|
.fade-scale-enter-active {
|
||||||
|
transition: all 0.28s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-scale-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-scale-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ///////////////////////////////////////////////
|
// ///////////////////////////////////////////////
|
||||||
|
@@ -13,15 +13,15 @@
|
|||||||
// zoom-fade
|
// zoom-fade
|
||||||
.zoom-fade-enter-active,
|
.zoom-fade-enter-active,
|
||||||
.zoom-fade-leave-active {
|
.zoom-fade-leave-active {
|
||||||
transition: transform 0.15s, opacity 0.2s ease-out;
|
transition: transform 0.2s, opacity 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.zoom-fade-enter-from {
|
.zoom-fade-enter-from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.97);
|
transform: scale(0.92);
|
||||||
}
|
}
|
||||||
|
|
||||||
.zoom-fade-leave-to {
|
.zoom-fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(1.03);
|
transform: scale(1.06);
|
||||||
}
|
}
|
||||||
|
@@ -39,4 +39,5 @@ export enum RouterTransitionEnum {
|
|||||||
FADE_SIDE = 'fade-slide',
|
FADE_SIDE = 'fade-slide',
|
||||||
FADE = 'fade',
|
FADE = 'fade',
|
||||||
FADE_BOTTOM = 'fade-bottom',
|
FADE_BOTTOM = 'fade-bottom',
|
||||||
|
FADE_SCALE = 'fade-scale',
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
import { ref, onBeforeUpdate } from 'vue';
|
import { ref, onBeforeUpdate, Ref } from 'vue';
|
||||||
|
|
||||||
export function useRefs() {
|
export function useRefs(): [Ref<HTMLElement[]>, (index: number) => (el: HTMLElement) => void] {
|
||||||
const refs = ref([] as Element[]);
|
const refs = ref<HTMLElement[]>([]);
|
||||||
|
|
||||||
onBeforeUpdate(() => {
|
onBeforeUpdate(() => {
|
||||||
refs.value = [];
|
refs.value = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
const setRefs = (index: number) => (el: Element) => {
|
const setRefs = (index: number) => (el: HTMLElement) => {
|
||||||
refs.value[index] = el;
|
refs.value[index] = el;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -51,6 +51,8 @@ const getFixed = computed(() => unref(getHeaderSetting).fixed);
|
|||||||
|
|
||||||
const getHeaderBgColor = computed(() => unref(getHeaderSetting).bgColor);
|
const getHeaderBgColor = computed(() => unref(getHeaderSetting).bgColor);
|
||||||
|
|
||||||
|
const getShowSearch = computed(() => unref(getHeaderSetting).showSearch);
|
||||||
|
|
||||||
const getShowRedo = computed(() => unref(getHeaderSetting).showRedo && unref(getShowMultipleTab));
|
const getShowRedo = computed(() => unref(getHeaderSetting).showRedo && unref(getShowMultipleTab));
|
||||||
|
|
||||||
const getUseLockPage = computed(() => unref(getHeaderSetting).useLockPage);
|
const getUseLockPage = computed(() => unref(getHeaderSetting).useLockPage);
|
||||||
@@ -87,6 +89,7 @@ export function useHeaderSetting() {
|
|||||||
getHeaderSetting,
|
getHeaderSetting,
|
||||||
|
|
||||||
getShowDoc,
|
getShowDoc,
|
||||||
|
getShowSearch,
|
||||||
getHeaderTheme,
|
getHeaderTheme,
|
||||||
getShowRedo,
|
getShowRedo,
|
||||||
getUseLockPage,
|
getUseLockPage,
|
||||||
|
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
62
src/layouts/default/content/index.vue
Normal file
62
src/layouts/default/content/index.vue
Normal file
@@ -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>
|
@@ -18,7 +18,7 @@ import { AppLogo } from '/@/components/Application';
|
|||||||
import UserDropdown from './UserDropdown';
|
import UserDropdown from './UserDropdown';
|
||||||
import LayoutMenu from '../menu';
|
import LayoutMenu from '../menu';
|
||||||
import LayoutBreadcrumb from './LayoutBreadcrumb.vue';
|
import LayoutBreadcrumb from './LayoutBreadcrumb.vue';
|
||||||
import LockAction from '../lock/LockAction';
|
import LockAction from './actions/LockAction';
|
||||||
import LayoutTrigger from '../LayoutTrigger';
|
import LayoutTrigger from '../LayoutTrigger';
|
||||||
import NoticeAction from './notice/NoticeActionItem.vue';
|
import NoticeAction from './notice/NoticeActionItem.vue';
|
||||||
import {
|
import {
|
||||||
@@ -28,6 +28,8 @@ import {
|
|||||||
LockOutlined,
|
LockOutlined,
|
||||||
BugOutlined,
|
BugOutlined,
|
||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
|
|
||||||
|
import { AppSearch } from '/@/components/Application';
|
||||||
import { useModal } from '/@/components/Modal';
|
import { useModal } from '/@/components/Modal';
|
||||||
|
|
||||||
import { useFullscreen } from '/@/hooks/web/useFullScreen';
|
import { useFullscreen } from '/@/hooks/web/useFullScreen';
|
||||||
@@ -200,6 +202,8 @@ export default defineComponent({
|
|||||||
function renderAction() {
|
function renderAction() {
|
||||||
return (
|
return (
|
||||||
<div class={`layout-header__action`}>
|
<div class={`layout-header__action`}>
|
||||||
|
{unref(isPc) && <AppSearch class="layout-header__action-item" />}
|
||||||
|
|
||||||
{unref(getUseErrorHandle) && unref(isPc) && (
|
{unref(getUseErrorHandle) && unref(isPc) && (
|
||||||
<TooltipItem title={t('layout.header.tooltipErrorLog')}>
|
<TooltipItem title={t('layout.header.tooltipErrorLog')}>
|
||||||
{() => (
|
{() => (
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
.multiple-tab-header {
|
.multiple-tab-header {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
transition: width 0.2s;
|
||||||
|
|
||||||
&.dark {
|
&.dark {
|
||||||
margin-left: -1px;
|
margin-left: -1px;
|
||||||
|
@@ -131,7 +131,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-icon {
|
&-icon,
|
||||||
|
span[role='img'] {
|
||||||
color: @text-color-base;
|
color: @text-color-base;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,9 +4,9 @@ import { defineComponent, unref, computed, ref } from 'vue';
|
|||||||
import { Layout, BackTop } from 'ant-design-vue';
|
import { Layout, BackTop } from 'ant-design-vue';
|
||||||
import LayoutHeader from './header/LayoutHeader';
|
import LayoutHeader from './header/LayoutHeader';
|
||||||
|
|
||||||
import LayoutContent from './content';
|
import LayoutContent from './content/index.vue';
|
||||||
import LayoutFooter from './footer';
|
import LayoutFooter from './footer';
|
||||||
import LayoutLockPage from './lock/index.vue';
|
import LayoutLockPage from '/@/views/sys/lock/index.vue';
|
||||||
import LayoutSideBar from './sider';
|
import LayoutSideBar from './sider';
|
||||||
import SettingBtn from './setting/index.vue';
|
import SettingBtn from './setting/index.vue';
|
||||||
import LayoutMultipleHeader from './header/LayoutMultipleHeader';
|
import LayoutMultipleHeader from './header/LayoutMultipleHeader';
|
||||||
|
@@ -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>
|
|
@@ -204,7 +204,6 @@ export default defineComponent({
|
|||||||
getCollapsedShowTitle,
|
getCollapsedShowTitle,
|
||||||
getMenuFixed,
|
getMenuFixed,
|
||||||
getCollapsed,
|
getCollapsed,
|
||||||
getShowSearch,
|
|
||||||
getCanDrag,
|
getCanDrag,
|
||||||
getTopMenuAlign,
|
getTopMenuAlign,
|
||||||
getAccordion,
|
getAccordion,
|
||||||
@@ -214,7 +213,12 @@ export default defineComponent({
|
|||||||
getSplit,
|
getSplit,
|
||||||
} = useMenuSetting();
|
} = useMenuSetting();
|
||||||
|
|
||||||
const { getShowHeader, getFixed: getHeaderFixed, getHeaderBgColor } = useHeaderSetting();
|
const {
|
||||||
|
getShowHeader,
|
||||||
|
getFixed: getHeaderFixed,
|
||||||
|
getHeaderBgColor,
|
||||||
|
getShowSearch,
|
||||||
|
} = useHeaderSetting();
|
||||||
|
|
||||||
const { getShowMultipleTab, getShowQuick } = useMultipleTabSetting();
|
const { getShowMultipleTab, getShowQuick } = useMultipleTabSetting();
|
||||||
|
|
||||||
@@ -274,10 +278,10 @@ export default defineComponent({
|
|||||||
}),
|
}),
|
||||||
renderSwitchItem(t('layout.setting.menuSearch'), {
|
renderSwitchItem(t('layout.setting.menuSearch'), {
|
||||||
handler: (e) => {
|
handler: (e) => {
|
||||||
baseHandler(HandlerEnum.MENU_SHOW_SEARCH, e);
|
baseHandler(HandlerEnum.HEADER_SEARCH, e);
|
||||||
},
|
},
|
||||||
def: unref(getShowSearch),
|
def: unref(getShowSearch),
|
||||||
disabled: !unref(getShowMenuRef),
|
disabled: !unref(getShowHeader),
|
||||||
}),
|
}),
|
||||||
renderSwitchItem(t('layout.setting.menuAccordion'), {
|
renderSwitchItem(t('layout.setting.menuAccordion'), {
|
||||||
handler: (e) => {
|
handler: (e) => {
|
||||||
|
@@ -28,6 +28,8 @@ export enum HandlerEnum {
|
|||||||
HEADER_THEME,
|
HEADER_THEME,
|
||||||
HEADER_FIXED,
|
HEADER_FIXED,
|
||||||
|
|
||||||
|
HEADER_SEARCH,
|
||||||
|
|
||||||
TABS_SHOW_QUICK,
|
TABS_SHOW_QUICK,
|
||||||
TABS_SHOW,
|
TABS_SHOW,
|
||||||
|
|
||||||
@@ -94,6 +96,7 @@ export const routerTransitionOptions = [
|
|||||||
RouterTransitionEnum.ZOOM_OUT,
|
RouterTransitionEnum.ZOOM_OUT,
|
||||||
RouterTransitionEnum.FADE_SIDE,
|
RouterTransitionEnum.FADE_SIDE,
|
||||||
RouterTransitionEnum.FADE_BOTTOM,
|
RouterTransitionEnum.FADE_BOTTOM,
|
||||||
|
RouterTransitionEnum.FADE_SCALE,
|
||||||
].map((item) => {
|
].map((item) => {
|
||||||
return {
|
return {
|
||||||
label: item,
|
label: item,
|
||||||
|
@@ -119,6 +119,9 @@ export function handler(event: HandlerEnum, value: any): DeepPartial<ProjectConf
|
|||||||
updateHeaderBgColor(value);
|
updateHeaderBgColor(value);
|
||||||
return { headerSetting: { bgColor: value } };
|
return { headerSetting: { bgColor: value } };
|
||||||
|
|
||||||
|
case HandlerEnum.HEADER_SEARCH:
|
||||||
|
return { headerSetting: { showSearch: value } };
|
||||||
|
|
||||||
case HandlerEnum.HEADER_FIXED:
|
case HandlerEnum.HEADER_FIXED:
|
||||||
return { headerSetting: { fixed: value } };
|
return { headerSetting: { fixed: value } };
|
||||||
|
|
||||||
|
@@ -100,7 +100,7 @@ export default defineComponent({
|
|||||||
flex: `0 0 ${width}`,
|
flex: `0 0 ${width}`,
|
||||||
maxWidth: width,
|
maxWidth: width,
|
||||||
minWidth: width,
|
minWidth: width,
|
||||||
transition: 'all 0.15s',
|
transition: 'all 0.2s',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@@ -30,7 +30,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<div>
|
||||||
<RouterView>
|
<RouterView>
|
||||||
{{
|
{{
|
||||||
default: ({ Component, route }: DefaultContext) => {
|
default: ({ Component, route }: DefaultContext) => {
|
||||||
@@ -65,7 +65,7 @@ export default defineComponent({
|
|||||||
}}
|
}}
|
||||||
</RouterView>
|
</RouterView>
|
||||||
{unref(getCanEmbedIFramePage) && <FrameLayout />}
|
{unref(getCanEmbedIFramePage) && <FrameLayout />}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
7
src/locales/lang/en/component/app.ts
Normal file
7
src/locales/lang/en/component/app.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default {
|
||||||
|
search: 'Search',
|
||||||
|
searchNotData: 'No search results yet',
|
||||||
|
toSearch: 'to search',
|
||||||
|
toNavigate: 'to navigate',
|
||||||
|
toClose: 'to close',
|
||||||
|
};
|
@@ -3,6 +3,7 @@ export default {
|
|||||||
dropdownItemDoc: 'Document',
|
dropdownItemDoc: 'Document',
|
||||||
dropdownItemLoginOut: 'Login Out',
|
dropdownItemLoginOut: 'Login Out',
|
||||||
|
|
||||||
|
search: 'Search',
|
||||||
tooltipErrorLog: 'Error log',
|
tooltipErrorLog: 'Error log',
|
||||||
tooltipLock: 'Lock screen',
|
tooltipLock: 'Lock screen',
|
||||||
tooltipNotify: 'Notification',
|
tooltipNotify: 'Notification',
|
||||||
|
@@ -40,7 +40,7 @@ export default {
|
|||||||
sidebarTheme: 'Menu theme',
|
sidebarTheme: 'Menu theme',
|
||||||
|
|
||||||
menuDrag: 'Drag Sidebar',
|
menuDrag: 'Drag Sidebar',
|
||||||
menuSearch: 'Sidebar search',
|
menuSearch: 'Menu search',
|
||||||
menuAccordion: 'Sidebar accordion',
|
menuAccordion: 'Sidebar accordion',
|
||||||
menuCollapse: 'Collapse menu',
|
menuCollapse: 'Collapse menu',
|
||||||
collapseMenuDisplayName: 'Collapse menu display name',
|
collapseMenuDisplayName: 'Collapse menu display name',
|
||||||
|
7
src/locales/lang/zh_CN/component/app.ts
Normal file
7
src/locales/lang/zh_CN/component/app.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default {
|
||||||
|
search: '搜索',
|
||||||
|
searchNotData: '暂无搜索结果',
|
||||||
|
toSearch: '确认',
|
||||||
|
toNavigate: '切换',
|
||||||
|
toClose: '关闭',
|
||||||
|
};
|
@@ -4,6 +4,7 @@ export default {
|
|||||||
dropdownItemLoginOut: '退出系统',
|
dropdownItemLoginOut: '退出系统',
|
||||||
|
|
||||||
// tooltip
|
// tooltip
|
||||||
|
search: '搜索',
|
||||||
tooltipErrorLog: '错误日志',
|
tooltipErrorLog: '错误日志',
|
||||||
tooltipLock: '锁定屏幕',
|
tooltipLock: '锁定屏幕',
|
||||||
tooltipNotify: '消息通知',
|
tooltipNotify: '消息通知',
|
||||||
|
@@ -39,7 +39,7 @@ export default {
|
|||||||
sidebarTheme: '菜单主题',
|
sidebarTheme: '菜单主题',
|
||||||
|
|
||||||
menuDrag: '侧边菜单拖拽',
|
menuDrag: '侧边菜单拖拽',
|
||||||
menuSearch: '侧边菜单搜索',
|
menuSearch: '菜单搜索',
|
||||||
menuAccordion: '侧边菜单手风琴模式',
|
menuAccordion: '侧边菜单手风琴模式',
|
||||||
menuCollapse: '折叠菜单',
|
menuCollapse: '折叠菜单',
|
||||||
collapseMenuDisplayName: '折叠菜单显示名称',
|
collapseMenuDisplayName: '折叠菜单显示名称',
|
||||||
|
@@ -45,13 +45,13 @@ async function getAsyncMenus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取深层扁平化菜单
|
// 获取深层扁平化菜单
|
||||||
export const getFlatMenus = async () => {
|
export const getFlatMenus = async (): Promise<Menu[]> => {
|
||||||
const menus = await getAsyncMenus();
|
const menus = await getAsyncMenus();
|
||||||
return flatMenus(menus);
|
return flatMenus(menus);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取菜单 树级
|
// 获取菜单 树级
|
||||||
export const getMenus = async () => {
|
export const getMenus = async (): Promise<Menu[]> => {
|
||||||
const menus = await getAsyncMenus();
|
const menus = await getAsyncMenus();
|
||||||
const routes = router.getRoutes();
|
const routes = router.getRoutes();
|
||||||
return !isBackMode() ? filter(menus, basicFilter(routes)) : menus;
|
return !isBackMode() ? filter(menus, basicFilter(routes)) : menus;
|
||||||
@@ -65,7 +65,7 @@ export async function getCurrentParentPath(currentPath: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取1级菜单,删除children
|
// 获取1级菜单,删除children
|
||||||
export async function getShallowMenus() {
|
export async function getShallowMenus(): Promise<Menu[]> {
|
||||||
const menus = await getAsyncMenus();
|
const menus = await getAsyncMenus();
|
||||||
const routes = router.getRoutes();
|
const routes = router.getRoutes();
|
||||||
const shallowMenuList = menus.map((item) => ({ ...item, children: undefined }));
|
const shallowMenuList = menus.map((item) => ({ ...item, children: undefined }));
|
||||||
|
@@ -3,6 +3,7 @@ import type { AppRouteRecordRaw, AppRouteModule } from '/@/router/types';
|
|||||||
import { PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '../constant';
|
import { PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '../constant';
|
||||||
|
|
||||||
import modules from 'globby!/@/router/routes/modules/**/*.@(ts)';
|
import modules from 'globby!/@/router/routes/modules/**/*.@(ts)';
|
||||||
|
import { PageEnum } from '/@/enums/pageEnum';
|
||||||
|
|
||||||
import { t } from '/@/hooks/web/useI18n';
|
import { t } from '/@/hooks/web/useI18n';
|
||||||
|
|
||||||
@@ -15,6 +16,15 @@ Object.keys(modules).forEach((key) => {
|
|||||||
|
|
||||||
export const asyncRoutes = [PAGE_NOT_FOUND_ROUTE, ...routeModuleList];
|
export const asyncRoutes = [PAGE_NOT_FOUND_ROUTE, ...routeModuleList];
|
||||||
|
|
||||||
|
export const RootRoute: AppRouteRecordRaw = {
|
||||||
|
path: '/',
|
||||||
|
name: 'Root',
|
||||||
|
redirect: PageEnum.BASE_HOME,
|
||||||
|
meta: {
|
||||||
|
title: 'Root',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const LoginRoute: AppRouteRecordRaw = {
|
export const LoginRoute: AppRouteRecordRaw = {
|
||||||
path: '/login',
|
path: '/login',
|
||||||
name: 'Login',
|
name: 'Login',
|
||||||
@@ -25,4 +35,4 @@ export const LoginRoute: AppRouteRecordRaw = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 基础路由 不用权限
|
// 基础路由 不用权限
|
||||||
export const basicRoutes = [LoginRoute, REDIRECT_ROUTE];
|
export const basicRoutes = [LoginRoute, RootRoute, REDIRECT_ROUTE];
|
||||||
|
@@ -70,6 +70,8 @@ const setting: ProjectConfig = {
|
|||||||
showDoc: true,
|
showDoc: true,
|
||||||
// Whether to show the notification button
|
// Whether to show the notification button
|
||||||
showNotice: true,
|
showNotice: true,
|
||||||
|
// Whether to display the menu search
|
||||||
|
showSearch: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Menu configuration
|
// Menu configuration
|
||||||
@@ -101,8 +103,6 @@ const setting: ProjectConfig = {
|
|||||||
split: false,
|
split: false,
|
||||||
// Top menu layout
|
// Top menu layout
|
||||||
topMenuAlign: 'center',
|
topMenuAlign: 'center',
|
||||||
// Hide the search box when the menu is collapsed
|
|
||||||
collapsedShowSearch: false,
|
|
||||||
// Fold trigger position
|
// Fold trigger position
|
||||||
trigger: TriggerEnum.HEADER,
|
trigger: TriggerEnum.HEADER,
|
||||||
// Turn on accordion mode, only show a menu
|
// Turn on accordion mode, only show a menu
|
||||||
|
3
src/types/config.d.ts
vendored
3
src/types/config.d.ts
vendored
@@ -17,7 +17,6 @@ export interface MenuSetting {
|
|||||||
type: MenuTypeEnum;
|
type: MenuTypeEnum;
|
||||||
theme: ThemeEnum;
|
theme: ThemeEnum;
|
||||||
topMenuAlign: 'start' | 'center' | 'end';
|
topMenuAlign: 'start' | 'center' | 'end';
|
||||||
collapsedShowSearch: boolean;
|
|
||||||
trigger: TriggerEnum;
|
trigger: TriggerEnum;
|
||||||
accordion: boolean;
|
accordion: boolean;
|
||||||
}
|
}
|
||||||
@@ -45,6 +44,8 @@ export interface HeaderSetting {
|
|||||||
showDoc: boolean;
|
showDoc: boolean;
|
||||||
// 显示消息中心按钮
|
// 显示消息中心按钮
|
||||||
showNotice: boolean;
|
showNotice: boolean;
|
||||||
|
|
||||||
|
showSearch: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LocaleSetting {
|
export interface LocaleSetting {
|
||||||
|
32
src/utils/factory/createAsyncComponent.tsx
Normal file
32
src/utils/factory/createAsyncComponent.tsx
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
@@ -124,7 +124,7 @@ export function filter<T = any>(
|
|||||||
tree: T[],
|
tree: T[],
|
||||||
func: (n: T) => boolean,
|
func: (n: T) => boolean,
|
||||||
config: Partial<TreeHelperConfig> = {}
|
config: Partial<TreeHelperConfig> = {}
|
||||||
) {
|
): T[] {
|
||||||
config = getConfig(config);
|
config = getConfig(config);
|
||||||
const children = config.children as string;
|
const children = config.children as string;
|
||||||
function listFilter(list: T[]) {
|
function listFilter(list: T[]) {
|
||||||
@@ -142,7 +142,7 @@ export function forEach<T = any>(
|
|||||||
tree: T[],
|
tree: T[],
|
||||||
func: (n: T) => any,
|
func: (n: T) => any,
|
||||||
config: Partial<TreeHelperConfig> = {}
|
config: Partial<TreeHelperConfig> = {}
|
||||||
) {
|
): void {
|
||||||
config = getConfig(config);
|
config = getConfig(config);
|
||||||
const list: any[] = [...tree];
|
const list: any[] = [...tree];
|
||||||
const { children } = config;
|
const { children } = config;
|
||||||
@@ -155,7 +155,7 @@ export function forEach<T = any>(
|
|||||||
/**
|
/**
|
||||||
* @description: 提取tree指定结构
|
* @description: 提取tree指定结构
|
||||||
*/
|
*/
|
||||||
export function treeMap(treeData: any[], opt: { children?: string; conversion: Fn }) {
|
export function treeMap<T = any>(treeData: T[], opt: { children?: string; conversion: Fn }): T[] {
|
||||||
return treeData.map((item) => treeMapEach(item, opt));
|
return treeData.map((item) => treeMapEach(item, opt));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
300
src/views/sys/lock/LockPage.vue
Normal file
300
src/views/sys/lock/LockPage.vue
Normal file
@@ -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>
|
@@ -1,300 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="prefixCls">
|
<transition name="fade-bottom" mode="out-in">
|
||||||
<div :class="`${prefixCls}__unlock`" @click="handleShowForm(false)" v-show="showDate">
|
<LockPage v-if="getIsLock" />
|
||||||
<LockOutlined />
|
</transition>
|
||||||
<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>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref, computed } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import { Alert, Input } from 'ant-design-vue';
|
import LockPage from './LockPage.vue';
|
||||||
|
import { getIsLock } from '/@/hooks/web/useLockPage';
|
||||||
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({
|
export default defineComponent({
|
||||||
name: 'LockPage',
|
name: 'Lock',
|
||||||
components: { Alert, LockOutlined, InputPassword: Input.Password },
|
components: { LockPage },
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
const passwordRef = ref('');
|
return { getIsLock };
|
||||||
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>
|
</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(10px);
|
|
||||||
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>
|
|
||||||
|
Reference in New Issue
Block a user