feat(组件): 缩略卡片组件&卡片列表业务组件&部分组件调整&useContainerShadow钩子
This commit is contained in:
parent
eba0879538
commit
eed7fdb0b2
|
@ -37,8 +37,8 @@
|
|||
"dependencies": {
|
||||
"@7polo/kity": "2.0.8",
|
||||
"@7polo/kityminder-core": "1.4.53",
|
||||
"@arco-design/web-vue": "^2.51.1",
|
||||
"@arco-themes/vue-ms-theme-default": "^0.0.29",
|
||||
"@arco-design/web-vue": "^2.51.2",
|
||||
"@arco-themes/vue-ms-theme-default": "^0.0.30",
|
||||
"@form-create/arco-design": "^3.1.23",
|
||||
"@types/color": "^3.0.4",
|
||||
"@vueuse/core": "^10.4.1",
|
||||
|
|
|
@ -83,7 +83,7 @@
|
|||
|
||||
onMounted(async () => {
|
||||
await getPublicKey();
|
||||
if (WHITE_LIST.find((el) => el.name === route.name) === undefined) {
|
||||
if (WHITE_LIST.find((el) => el.path === window.location.hash.split('#')[1]) === undefined) {
|
||||
await checkIsLogin();
|
||||
}
|
||||
const { height } = useWindowSize();
|
||||
|
|
|
@ -96,6 +96,11 @@
|
|||
padding: 8px 0;
|
||||
}
|
||||
}
|
||||
.ms-modal-no-padding {
|
||||
.arco-modal-body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
.ms-modal-upload {
|
||||
.arco-modal-body {
|
||||
padding: 2px 0;
|
||||
|
@ -178,6 +183,12 @@
|
|||
.btn-outline-sec-active();
|
||||
.btn-outline-sec-disabled();
|
||||
}
|
||||
.arco-btn-outline--danger {
|
||||
.btn-outline-danger-default();
|
||||
.btn-outline-danger-hover();
|
||||
.btn-outline-danger-active();
|
||||
.btn-outline-danger-disabled();
|
||||
}
|
||||
|
||||
/** 输入框,选择器,文本域 **/
|
||||
.arco-select {
|
||||
|
@ -269,6 +280,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.arco-textarea {
|
||||
.ms-scroll-bar();
|
||||
}
|
||||
|
||||
/** form-item **/
|
||||
.arco-form-item-content-flex {
|
||||
|
@ -696,6 +710,9 @@
|
|||
}
|
||||
|
||||
/** tooltip **/
|
||||
.arco-tooltip-content {
|
||||
@apply break-all;
|
||||
}
|
||||
.arco-trigger-arrow {
|
||||
border-bottom-right-radius: var(--border-radius-mini) !important;
|
||||
}
|
||||
|
@ -726,18 +743,3 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
/** 隐藏drawer-mask **/
|
||||
.ms-drawer-no-mask {
|
||||
left: auto;
|
||||
.arco-drawer {
|
||||
box-shadow: -1px 0 4px 0 rgb(2 2 2 / 10%);
|
||||
&-header {
|
||||
.arco-drawer-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
@border-radius-large: 12px;
|
||||
|
||||
@color-white: #fff;
|
||||
@color-fill-2: rgb(var(--primary-9));
|
||||
@color-text-5: #aeaeb2;
|
||||
|
||||
/** 常用颜色类 **/
|
||||
|
@ -59,6 +58,10 @@
|
|||
color: var(--color-text-4) !important;
|
||||
}
|
||||
}
|
||||
.btn-outline-danger-default() {
|
||||
border-color: rgb(var(--danger-6)) !important;
|
||||
color: rgb(var(--danger-6)) !important;
|
||||
}
|
||||
.btn-outline-danger-hover() {
|
||||
&:not(:disabled):hover {
|
||||
border-color: rgb(var(--danger-5)) !important;
|
||||
|
@ -183,3 +186,36 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 容器内部上下阴影类 **/
|
||||
.ms-container--shadow() {
|
||||
@apply relative;
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
box-shadow: none;
|
||||
transition: box-shadow 0.1s cubic-bezier(0, 0, 1, 1);
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
}
|
||||
&-shadow-top::before {
|
||||
box-shadow: inset 0 10px 6px -10px rgb(0 0 0 / 15%);
|
||||
}
|
||||
&::after {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
box-shadow: none;
|
||||
transition: box-shadow 0.1s cubic-bezier(0, 0, 1, 1);
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
}
|
||||
&-shadow-bottom::after {
|
||||
box-shadow: inset 0 -10px 6px -10px rgb(0 0 0 / 15%);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,240 @@
|
|||
<template>
|
||||
<div ref="msCardListContainerRef" :class="['ms-card-list-container', containerStatusClass]">
|
||||
<div
|
||||
ref="msCardListRef"
|
||||
class="ms-card-list"
|
||||
:style="{ 'grid-template-columns': `repeat(auto-fill, minmax(${props.cardMinWidth}px, 1fr))` }"
|
||||
>
|
||||
<div v-if="topLoading" class="ms-card-list-loading">
|
||||
<a-spin :loading="topLoading"></a-spin>
|
||||
</div>
|
||||
<div
|
||||
v-for="(item, index) in props.mode === 'remote' ? remoteList : props.list"
|
||||
:key="item.key"
|
||||
class="ms-card-list-item"
|
||||
>
|
||||
<slot name="item" :item="item" :index="index"></slot>
|
||||
</div>
|
||||
<div v-if="bottomLoading" class="ms-card-list-loading">
|
||||
<a-spin :loading="bottomLoading"></a-spin>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { type Ref, ref, watch, nextTick, onMounted, watchEffect, computed } from 'vue';
|
||||
import { useResizeObserver } from '@vueuse/core';
|
||||
import { debounce } from 'lodash-es';
|
||||
import useContainerShadow from '@/hooks/useContainerShadow';
|
||||
|
||||
import type { TableQueryParams, CommonList } from '@/models/common';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
mode?: 'static' | 'remote'; // 模式,静态数据模式或者远程数据模式,默认静态数据模式,静态数据模式下,需要传入list;远程数据模式下,需要传入api请求函数
|
||||
list?: any[];
|
||||
cardMinWidth: number; // 卡片最小宽度px
|
||||
shadowLimit: number; // 滚动距离高度,用于计算顶部底部阴影
|
||||
remoteParams?: Record<string, any>; // 远程数据模式下,请求数据的参数
|
||||
remoteFunc?: (v: TableQueryParams) => Promise<CommonList<any>>; // 远程数据模式下,请求数据的函数
|
||||
}>(),
|
||||
{
|
||||
mode: 'static',
|
||||
}
|
||||
);
|
||||
|
||||
const msCardListRef: Ref<HTMLElement | null> = ref(null);
|
||||
const msCardListContainerRef: Ref<HTMLElement | null> = ref(null);
|
||||
|
||||
const { isArrivedTop, isArrivedBottom, isInitListener, containerStatusClass, setContainer, initScrollListener } =
|
||||
useContainerShadow({
|
||||
overHeight: props.shadowLimit,
|
||||
containerClassName: 'ms-card-list-container',
|
||||
});
|
||||
|
||||
/**
|
||||
* 初始化列表滚动监听
|
||||
* @param arr 列表数组
|
||||
*/
|
||||
function initListListener(arr?: any[]) {
|
||||
if (arr && arr.length > 0 && !isInitListener.value) {
|
||||
nextTick(() => {
|
||||
if (msCardListRef.value) {
|
||||
setContainer(msCardListRef.value);
|
||||
initScrollListener();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (arr && arr.length > 0) {
|
||||
nextTick(() => {
|
||||
if (msCardListRef.value) {
|
||||
// 为了在列表数据滚动加载时,能够正确判断是否滚动到底部,因为此时没有触发滚动事件,而在加载前触发了滚动到底部的判断
|
||||
const listContent = msCardListRef.value;
|
||||
const { scrollTop, scrollHeight, clientHeight } = listContent;
|
||||
isArrivedBottom.value = scrollHeight - clientHeight - scrollTop < props.shadowLimit;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.list,
|
||||
(val) => {
|
||||
if (props.mode === 'static') {
|
||||
initListListener(val);
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
const listSize = ref(0);
|
||||
const listPage = ref(0);
|
||||
const listTotal = ref(0);
|
||||
const remoteList = ref<any[]>([]);
|
||||
const noMore = computed(() => {
|
||||
return listSize.value * (listPage.value - 1) + remoteList.value.length >= listTotal.value;
|
||||
});
|
||||
const isInit = ref(false);
|
||||
|
||||
const topLoading = ref(false);
|
||||
const bottomLoading = ref(false);
|
||||
|
||||
/**
|
||||
* 加载上一页
|
||||
*/
|
||||
async function loadPrevList() {
|
||||
try {
|
||||
if (props.mode === 'remote' && typeof props.remoteFunc === 'function') {
|
||||
topLoading.value = true;
|
||||
listPage.value -= 1;
|
||||
const res = await props.remoteFunc({
|
||||
current: listPage.value,
|
||||
pageSize: listSize.value,
|
||||
...(props.remoteParams || {}),
|
||||
});
|
||||
remoteList.value = res.list;
|
||||
listTotal.value = res.total;
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
topLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载下一页
|
||||
*/
|
||||
async function loadNextList() {
|
||||
try {
|
||||
if (props.mode === 'remote' && typeof props.remoteFunc === 'function') {
|
||||
bottomLoading.value = true;
|
||||
listPage.value += 1;
|
||||
const res = await props.remoteFunc({
|
||||
current: listPage.value,
|
||||
pageSize: listSize.value,
|
||||
});
|
||||
remoteList.value = res.list;
|
||||
listTotal.value = res.total;
|
||||
bottomLoading.value = false;
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
bottomLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算列表每页显示数量
|
||||
* @param width 容器宽度
|
||||
* @param height 容器高度
|
||||
*/
|
||||
async function computedListSize(width?: number, height?: number) {
|
||||
let clientWidth = 0;
|
||||
let clientHeight = 0;
|
||||
if (width !== undefined && height !== undefined) {
|
||||
clientWidth = width;
|
||||
clientHeight = height;
|
||||
} else if (msCardListContainerRef.value && msCardListRef.value) {
|
||||
clientWidth = msCardListRef.value.clientWidth;
|
||||
clientHeight = msCardListContainerRef.value.clientHeight;
|
||||
}
|
||||
// 最大列数 = (容器宽度 + gap) / (最小卡片宽度 + gap)
|
||||
const maxCols = Math.floor((clientWidth + 24) / (props.cardMinWidth + 24));
|
||||
// 最大行数 =(容器高度 + gap) / (最小卡片宽度 + gap)。因为卡片宽高是 1:1,所以行数和列数相同。这里+2是为了在视图区域外多加载两行,以避免滚动条不出现
|
||||
const maxRows = Math.round((clientHeight + 24) / (props.cardMinWidth + 24)) + 2;
|
||||
listSize.value = maxCols * maxRows;
|
||||
setTimeout(() => {
|
||||
// 设置 400ms 后初始化完成,避免 useResizeObserver 一开始就触发,因为useResizeObserver使用了debounce-300ms,所以会有延迟
|
||||
isInit.value = true;
|
||||
}, 400);
|
||||
// 列表显示数量发生变化,重置列表页码,重新加载列表
|
||||
listPage.value = 0;
|
||||
await loadNextList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 容器大小变化监听,列表的每页显示数量会根据容器大小变化而变化,计算完成后会重新加载列表
|
||||
*/
|
||||
useResizeObserver(
|
||||
msCardListContainerRef,
|
||||
debounce((entries) => {
|
||||
const entry = entries[0];
|
||||
const { width, height } = entry.contentRect;
|
||||
if (isInit.value) {
|
||||
computedListSize(width, height);
|
||||
}
|
||||
}, 300)
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.mode === 'remote') {
|
||||
await computedListSize();
|
||||
initListListener(remoteList.value);
|
||||
}
|
||||
});
|
||||
|
||||
watchEffect(async () => {
|
||||
// 远程数据模式下,滚动到顶部或者底部时,加载上一页或者下一页
|
||||
if (props.mode === 'remote' && typeof props.remoteFunc === 'function') {
|
||||
if (isArrivedTop.value && !isArrivedBottom.value && listPage.value > 1 && !topLoading.value) {
|
||||
// 滚动到顶部且未滚动到底部(也就是数据量大于 1 页),且不是第一页,且不是正在加载上一页,则加载上一页
|
||||
loadPrevList();
|
||||
} else if (isArrivedBottom.value && !isArrivedTop.value && !noMore.value && !bottomLoading.value) {
|
||||
// 滚动到底部且未滚动到顶部(也就是数据量大于 1 页),且不是正在加载下一页,则加载下一页
|
||||
loadNextList();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ms-card-list-container {
|
||||
@apply overflow-hidden;
|
||||
.ms-container--shadow();
|
||||
.ms-card-list {
|
||||
@apply grid h-full overflow-auto;
|
||||
|
||||
.ms-scroll-bar();
|
||||
|
||||
gap: 24px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(102px, 1fr));
|
||||
.ms-card-list-item {
|
||||
@apply relative w-full;
|
||||
|
||||
aspect-ratio: 1/1;
|
||||
}
|
||||
.ms-card-list-loading {
|
||||
@apply col-span-full flex items-center justify-center;
|
||||
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -11,6 +11,8 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
text: string;
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
<template>
|
||||
<div :class="['ms-thumbnail-card', `ms-thumbnail-card--${props.mode}`]" @click="handleCardClick">
|
||||
<div class="ms-thumbnail-card-content">
|
||||
<div class="ms-thumbnail-card-more">
|
||||
<MsTableMoreAction v-if="props.moreActions" :list="props.moreActions" @select="handleMoreActionSelect" />
|
||||
</div>
|
||||
<img v-if="fileType === 'image'" :src="props.url" class="absolute top-0" />
|
||||
<MsIcon v-else :type="FileIconMap[fileType][UploadStatus.done]" class="absolute top-0 h-full w-full p-[24px]" />
|
||||
<div class="ms-thumbnail-card-footer">
|
||||
{{ props.footerText }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { UploadStatus } from '@/enums/uploadEnum';
|
||||
import { getFileEnum, FileIconMap } from '@/components/pure/ms-upload/iconMap';
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
|
||||
|
||||
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
mode?: 'default' | 'hover'; // 默认模式和hover模式,hover 模式时会在 hover 卡片时显示 footer;default 则是一直显示 footer
|
||||
type: string;
|
||||
url?: string;
|
||||
footerText?: string;
|
||||
moreActions?: ActionsItem[];
|
||||
}>(),
|
||||
{
|
||||
mode: 'default',
|
||||
}
|
||||
);
|
||||
const emit = defineEmits<{
|
||||
(e: 'click'): void;
|
||||
(e: 'actionSelect', item: ActionsItem): void;
|
||||
}>();
|
||||
|
||||
const fileType = computed(() => {
|
||||
if (props.type) {
|
||||
return getFileEnum(`/${props.type.toLowerCase()}`);
|
||||
}
|
||||
return 'unknown';
|
||||
});
|
||||
|
||||
function handleCardClick() {
|
||||
emit('click');
|
||||
}
|
||||
|
||||
function handleMoreActionSelect(item: ActionsItem) {
|
||||
emit('actionSelect', item);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ms-thumbnail-card {
|
||||
@apply relative w-full;
|
||||
|
||||
aspect-ratio: 1/1;
|
||||
.ms-thumbnail-card-content {
|
||||
@apply absolute bottom-0 left-0 right-0 top-0 inline-block flex-grow cursor-pointer overflow-hidden;
|
||||
|
||||
min-width: 102px;
|
||||
border-radius: var(--border-radius-small);
|
||||
background-color: var(--color-text-n9);
|
||||
transition: all 0.2s;
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
padding-bottom: 100%; /* 高度与宽度 1:1 */
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.ms-thumbnail-card-more {
|
||||
@apply visible;
|
||||
}
|
||||
}
|
||||
.ms-thumbnail-card-more {
|
||||
@apply invisible absolute bg-white;
|
||||
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
z-index: 1;
|
||||
padding: 4px;
|
||||
border-radius: var(--border-radius-small);
|
||||
&:hover {
|
||||
color: rgb(var(--primary-5));
|
||||
background-color: rgb(var(--primary-1));
|
||||
}
|
||||
}
|
||||
.ms-thumbnail-card-footer {
|
||||
@apply absolute w-full text-center;
|
||||
|
||||
bottom: 0;
|
||||
padding: 2px 0;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
background-color: #00000050;
|
||||
}
|
||||
}
|
||||
.ms-thumbnail-card--default {
|
||||
&:hover {
|
||||
background-color: rgb(50 50 51 / 10%);
|
||||
}
|
||||
.ms-thumbnail-card-footer {
|
||||
@apply visible;
|
||||
|
||||
background-color: #32323330;
|
||||
}
|
||||
}
|
||||
.ms-thumbnail-card--hover {
|
||||
&:hover {
|
||||
.ms-thumbnail-card-footer {
|
||||
@apply visible;
|
||||
}
|
||||
}
|
||||
.ms-thumbnail-card-footer {
|
||||
@apply invisible;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,65 +1,68 @@
|
|||
<template>
|
||||
<a-tree
|
||||
v-bind="props"
|
||||
ref="treeRef"
|
||||
v-model:expanded-keys="expandedKeys"
|
||||
:selected-keys="selectedKeys"
|
||||
:data="treeData"
|
||||
class="ms-tree"
|
||||
@drop="onDrop"
|
||||
@select="select"
|
||||
>
|
||||
<template v-if="$slots['title']" #title="_props">
|
||||
<slot name="title" v-bind="_props"></slot>
|
||||
</template>
|
||||
<template v-if="$slots['extra']" #extra="_props">
|
||||
<div
|
||||
:class="[
|
||||
'ms-tree-node-extra',
|
||||
innerFocusNodeKey === _props[props.fieldNames.key] ? 'ms-tree-node-extra--focus' : '',
|
||||
]"
|
||||
>
|
||||
<div
|
||||
class="ml-[-4px] flex h-[32px] items-center rounded-[var(--border-radius-small)] bg-[rgb(var(--primary-1))]"
|
||||
>
|
||||
<slot name="extra" v-bind="_props"></slot>
|
||||
<MsTableMoreAction
|
||||
v-if="props.nodeMoreActions"
|
||||
:list="props.nodeMoreActions"
|
||||
trigger="click"
|
||||
@select="handleNodeMoreSelect($event, _props)"
|
||||
@close="moreActionsClose"
|
||||
>
|
||||
<MsButton
|
||||
type="text"
|
||||
size="mini"
|
||||
class="ms-tree-node-extra__more"
|
||||
@click="innerFocusNodeKey = _props[props.fieldNames.key]"
|
||||
>
|
||||
<MsIcon type="icon-icon_more_outlined" size="14" class="text-[var(--color-text-4)]" />
|
||||
</MsButton>
|
||||
</MsTableMoreAction>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-tree>
|
||||
<slot name="empty">
|
||||
<div
|
||||
v-show="treeData.length === 0 && props.emptyText"
|
||||
class="rounded-[var(--border-radius-small)] bg-[var(--color-fill-1)] p-[8px] text-[12px] text-[var(--color-text-4)]"
|
||||
<div ref="treeContainerRef" :class="['ms-tree-container', containerStatusClass]">
|
||||
<a-tree
|
||||
v-bind="props"
|
||||
ref="treeRef"
|
||||
v-model:expanded-keys="expandedKeys"
|
||||
:selected-keys="selectedKeys"
|
||||
:data="treeData"
|
||||
class="ms-tree"
|
||||
@drop="onDrop"
|
||||
@select="select"
|
||||
>
|
||||
{{ props.emptyText }}
|
||||
</div>
|
||||
</slot>
|
||||
<template v-if="$slots['title']" #title="_props">
|
||||
<slot name="title" v-bind="_props"></slot>
|
||||
</template>
|
||||
<template v-if="$slots['extra']" #extra="_props">
|
||||
<div
|
||||
:class="[
|
||||
'ms-tree-node-extra',
|
||||
innerFocusNodeKey === _props[props.fieldNames.key] ? 'ms-tree-node-extra--focus' : '',
|
||||
]"
|
||||
>
|
||||
<div
|
||||
class="ml-[-4px] flex h-[32px] items-center rounded-[var(--border-radius-small)] bg-[rgb(var(--primary-1))]"
|
||||
>
|
||||
<slot name="extra" v-bind="_props"></slot>
|
||||
<MsTableMoreAction
|
||||
v-if="props.nodeMoreActions"
|
||||
:list="props.nodeMoreActions"
|
||||
trigger="click"
|
||||
@select="handleNodeMoreSelect($event, _props)"
|
||||
@close="moreActionsClose"
|
||||
>
|
||||
<MsButton
|
||||
type="text"
|
||||
size="mini"
|
||||
class="ms-tree-node-extra__more"
|
||||
@click="innerFocusNodeKey = _props[props.fieldNames.key]"
|
||||
>
|
||||
<MsIcon type="icon-icon_more_outlined" size="14" class="text-[var(--color-text-4)]" />
|
||||
</MsButton>
|
||||
</MsTableMoreAction>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-tree>
|
||||
<slot name="empty">
|
||||
<div
|
||||
v-show="treeData.length === 0 && props.emptyText"
|
||||
class="rounded-[var(--border-radius-small)] bg-[var(--color-fill-1)] p-[8px] text-[12px] text-[var(--color-text-4)]"
|
||||
>
|
||||
{{ props.emptyText }}
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeMount, ref, h, watch, Ref, watchEffect } from 'vue';
|
||||
import { onBeforeMount, ref, h, watch, Ref, watchEffect, nextTick } from 'vue';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { mapTree } from '@/utils/index';
|
||||
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import useContainerShadow from '@/hooks/useContainerShadow';
|
||||
|
||||
import type { MsTreeNodeData, MsTreeFieldNames, MsTreeSelectedData } from './types';
|
||||
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||
|
@ -80,6 +83,7 @@
|
|||
nodeMoreActions?: ActionsItem[]; // 节点展示在省略号按钮内的更多操作
|
||||
expandAll?: boolean; // 是否展开/折叠所有节点,true 为全部展开,false 为全部折叠
|
||||
emptyText?: string; // 空数据时的文案
|
||||
virtualListProps?: Record<string, unknown>; // 虚拟滚动列表的属性
|
||||
}>(),
|
||||
{
|
||||
searchDebounce: 300,
|
||||
|
@ -109,7 +113,12 @@
|
|||
(e: 'moreActionsClose'): void;
|
||||
}>();
|
||||
|
||||
const treeContainerRef: Ref = ref(null);
|
||||
const treeRef: Ref = ref(null);
|
||||
const { isInitListener, containerStatusClass, setContainer, initScrollListener } = useContainerShadow({
|
||||
overHeight: 32,
|
||||
containerClassName: 'ms-tree-container',
|
||||
});
|
||||
const originalTreeData = ref<MsTreeNodeData[]>([]);
|
||||
|
||||
function init() {
|
||||
|
@ -129,6 +138,19 @@
|
|||
node.disabled = false;
|
||||
return node;
|
||||
});
|
||||
nextTick(() => {
|
||||
if (props.defaultExpandAll) {
|
||||
treeRef.value?.expandAll(true);
|
||||
}
|
||||
if (!isInitListener.value && treeRef.value) {
|
||||
setContainer(
|
||||
props.virtualListProps?.height
|
||||
? (treeRef.value.$el.querySelector('.arco-virtual-list') as HTMLElement)
|
||||
: treeRef.value.$el
|
||||
);
|
||||
initScrollListener();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
|
@ -318,86 +340,90 @@
|
|||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.ms-tree {
|
||||
.arco-tree-node {
|
||||
border-radius: var(--border-radius-small);
|
||||
&:hover {
|
||||
background-color: rgb(var(--primary-1));
|
||||
}
|
||||
.arco-tree-node-minus-icon,
|
||||
.arco-tree-node-plus-icon {
|
||||
border: 1px solid var(--color-text-4);
|
||||
border-radius: var(--border-radius-mini);
|
||||
background-color: white;
|
||||
&::after,
|
||||
&::before {
|
||||
background-color: var(--color-text-4);
|
||||
}
|
||||
}
|
||||
.arco-tree-node-switcher {
|
||||
.arco-tree-node-switcher-icon {
|
||||
@apply flex;
|
||||
|
||||
color: var(--color-text-4);
|
||||
}
|
||||
}
|
||||
.arco-tree-node-title {
|
||||
.ms-tree-container {
|
||||
.ms-container--shadow();
|
||||
.ms-tree {
|
||||
.ms-scroll-bar();
|
||||
.arco-tree-node {
|
||||
border-radius: var(--border-radius-small);
|
||||
&:hover {
|
||||
background-color: rgb(var(--primary-1));
|
||||
+ .ms-tree-node-extra {
|
||||
@apply block;
|
||||
}
|
||||
}
|
||||
.arco-tree-node-drag-icon {
|
||||
.arco-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ms-tree-node-extra {
|
||||
@apply relative hidden;
|
||||
&:hover {
|
||||
@apply block;
|
||||
}
|
||||
.ms-tree-node-extra__btn,
|
||||
.ms-tree-node-extra__more {
|
||||
padding: 4px;
|
||||
.arco-tree-node-minus-icon,
|
||||
.arco-tree-node-plus-icon {
|
||||
border: 1px solid var(--color-text-4);
|
||||
border-radius: var(--border-radius-mini);
|
||||
background-color: white;
|
||||
&::after,
|
||||
&::before {
|
||||
background-color: var(--color-text-4);
|
||||
}
|
||||
}
|
||||
.arco-tree-node-switcher {
|
||||
.arco-tree-node-switcher-icon {
|
||||
@apply flex;
|
||||
|
||||
color: var(--color-text-4);
|
||||
}
|
||||
}
|
||||
.arco-tree-node-title {
|
||||
&:hover {
|
||||
background-color: rgb(var(--primary-9));
|
||||
background-color: rgb(var(--primary-1));
|
||||
+ .ms-tree-node-extra {
|
||||
@apply block;
|
||||
}
|
||||
}
|
||||
.arco-tree-node-drag-icon {
|
||||
.arco-icon {
|
||||
color: rgb(var(--primary-5));
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ms-tree-node-extra__more {
|
||||
margin-right: 4px;
|
||||
.ms-tree-node-extra {
|
||||
@apply relative hidden;
|
||||
&:hover {
|
||||
@apply block;
|
||||
}
|
||||
.ms-tree-node-extra__btn,
|
||||
.ms-tree-node-extra__more {
|
||||
padding: 4px;
|
||||
border-radius: var(--border-radius-mini);
|
||||
&:hover {
|
||||
background-color: rgb(var(--primary-9));
|
||||
.arco-icon {
|
||||
color: rgb(var(--primary-5));
|
||||
}
|
||||
}
|
||||
}
|
||||
.ms-tree-node-extra__more {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
.ms-tree-node-extra--focus {
|
||||
@apply block;
|
||||
}
|
||||
.arco-tree-node-custom-icon {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
.ms-tree-node-extra--focus {
|
||||
@apply block;
|
||||
}
|
||||
.arco-tree-node-custom-icon {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
.arco-tree-node-selected {
|
||||
.arco-tree-node-minus-icon,
|
||||
.arco-tree-node-plus-icon {
|
||||
border: 1px solid rgb(var(--primary-5));
|
||||
border-radius: var(--border-radius-mini);
|
||||
background-color: white;
|
||||
&::after,
|
||||
&::before {
|
||||
background-color: rgb(var(--primary-5));
|
||||
.arco-tree-node-selected {
|
||||
.arco-tree-node-minus-icon,
|
||||
.arco-tree-node-plus-icon {
|
||||
border: 1px solid rgb(var(--primary-5));
|
||||
border-radius: var(--border-radius-mini);
|
||||
background-color: white;
|
||||
&::after,
|
||||
&::before {
|
||||
background-color: rgb(var(--primary-5));
|
||||
}
|
||||
}
|
||||
}
|
||||
.arco-tree-node-switcher-icon .arco-icon,
|
||||
.arco-tree-node-title {
|
||||
font-weight: 500 !important;
|
||||
color: rgb(var(--primary-5));
|
||||
* {
|
||||
.arco-tree-node-switcher-icon .arco-icon,
|
||||
.arco-tree-node-title {
|
||||
font-weight: 500 !important;
|
||||
color: rgb(var(--primary-5));
|
||||
* {
|
||||
color: rgb(var(--primary-5));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,17 +20,18 @@
|
|||
<div v-if="props.switchProps?.showSwitch" class="flex flex-row items-center justify-center">
|
||||
<a-switch v-model="switchEnable" class="mr-1" size="small" />
|
||||
<a-tooltip v-if="props.switchProps?.switchTooltip" :content="t(props.switchProps?.switchTooltip)">
|
||||
<span class="flex items-center"
|
||||
><span class="mr-1">{{ props.switchProps?.switchName }}</span>
|
||||
<span class="mt-[2px]"
|
||||
><IconQuestionCircle class="h-[16px] w-[16px] text-[rgb(var(--primary-5))]" /></span
|
||||
></span>
|
||||
<span class="flex items-center">
|
||||
<span class="mr-1">{{ props.switchProps?.switchName }}</span>
|
||||
<span class="mt-[2px]">
|
||||
<IconQuestionCircle class="h-[16px] w-[16px] text-[rgb(var(--primary-5))]" />
|
||||
</span>
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<a-button v-if="showCancel" type="secondary" @click="handleCancel">{{
|
||||
props.cancelText ? t(props.cancelText) : t('ms.dialog.cancel')
|
||||
}}</a-button>
|
||||
<a-button v-if="showCancel" type="secondary" @click="handleCancel">
|
||||
{{ props.cancelText ? t(props.cancelText) : t('ms.dialog.cancel') }}
|
||||
</a-button>
|
||||
<!-- 自定义确认与取消之间其他按钮可以直接使用loading按钮插槽 -->
|
||||
<slot name="self-button"></slot>
|
||||
<a-button
|
||||
|
@ -73,7 +74,7 @@
|
|||
}> & {
|
||||
dialogSize: SizeType; // 弹窗的宽度尺寸 medium large small
|
||||
title: string;
|
||||
confirm?: (enable: boolean | undefined) => void; // 确定
|
||||
confirm?: (enable: boolean | undefined) => Promise<any>; // 确定
|
||||
visible: boolean;
|
||||
close: () => void;
|
||||
};
|
||||
|
@ -137,6 +138,7 @@
|
|||
await props.confirm(switchEnable.value);
|
||||
confirmLoading.value = false;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
confirmLoading.value = false;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
v-bind="props"
|
||||
ref="listRef"
|
||||
:data="data"
|
||||
:class="['ms-list', listStatusClass]"
|
||||
:class="['ms-list', containerStatusClass]"
|
||||
@reach-bottom="handleReachBottom"
|
||||
>
|
||||
<template #item="{ item, index }">
|
||||
|
@ -59,11 +59,12 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, Ref, ref, watch } from 'vue';
|
||||
import { nextTick, Ref, ref, watch } from 'vue';
|
||||
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useContainerShadow from '@/hooks/useContainerShadow';
|
||||
|
||||
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||
|
||||
|
@ -136,23 +137,12 @@
|
|||
}
|
||||
|
||||
const listRef: Ref = ref(null);
|
||||
const isArrivedTop = ref(true);
|
||||
const isArrivedBottom = ref(true);
|
||||
const isInitListener = ref(false);
|
||||
|
||||
/**
|
||||
* 监听列表内容区域滚动,以切换顶部底部阴影
|
||||
* @param event 滚动事件
|
||||
*/
|
||||
function listenScroll(event: Event) {
|
||||
if (event.target) {
|
||||
const listContent = event.target as HTMLElement;
|
||||
const { scrollTop, scrollHeight, clientHeight } = listContent;
|
||||
const scrollBottom = scrollHeight - clientHeight - scrollTop;
|
||||
isArrivedTop.value = scrollTop < props.itemHeight;
|
||||
isArrivedBottom.value = scrollBottom < props.itemHeight;
|
||||
}
|
||||
}
|
||||
const { isArrivedBottom, isInitListener, containerStatusClass, setContainer, initScrollListener } =
|
||||
useContainerShadow({
|
||||
overHeight: props.itemHeight,
|
||||
containerClassName: 'ms-list',
|
||||
});
|
||||
|
||||
watch(
|
||||
props.data,
|
||||
|
@ -160,16 +150,18 @@
|
|||
if (props.data.length > 0 && !isInitListener.value) {
|
||||
nextTick(() => {
|
||||
const listContent = listRef.value?.$el.querySelector('.arco-list-content');
|
||||
isInitListener.value = true;
|
||||
listContent.addEventListener('scroll', listenScroll);
|
||||
setContainer(listContent);
|
||||
initScrollListener();
|
||||
});
|
||||
}
|
||||
if (props.data.length > 0) {
|
||||
nextTick(() => {
|
||||
// 为了在列表数据滚动加载时,能够正确判断是否滚动到底部,因为此时没有触发滚动事件,而在加载前触发了滚动到底部的判断
|
||||
const listContent = listRef.value?.$el.querySelector('.arco-list-content');
|
||||
const { scrollTop, scrollHeight, clientHeight } = listContent;
|
||||
isArrivedBottom.value = scrollHeight - clientHeight - scrollTop < props.itemHeight;
|
||||
});
|
||||
}
|
||||
nextTick(() => {
|
||||
// 为了在列表数据滚动加载时,能够正确判断是否滚动到底部,因为此时没有触发滚动事件,而在加载前触发了滚动到底部的判断
|
||||
const listContent = listRef.value?.$el.querySelector('.arco-list-content');
|
||||
const { scrollTop, scrollHeight, clientHeight } = listContent;
|
||||
isArrivedBottom.value = scrollHeight - clientHeight - scrollTop < props.itemHeight;
|
||||
});
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
|
@ -181,36 +173,11 @@
|
|||
emit('reachBottom');
|
||||
}
|
||||
}
|
||||
|
||||
const listStatusClass = computed(() => {
|
||||
if (isArrivedTop.value && isArrivedBottom.value) {
|
||||
// 内容不足一屏,不展示阴影
|
||||
return 'ms-list-hidden-shadow';
|
||||
}
|
||||
if (isArrivedTop.value) {
|
||||
// 滚动到顶部,隐藏顶部阴影
|
||||
return 'ms-list--hidden-top-shadow';
|
||||
}
|
||||
if (isArrivedBottom.value) {
|
||||
// 滚动到底部,隐藏底部阴影
|
||||
return 'ms-list--hidden-bottom-shadow';
|
||||
}
|
||||
// 滚动到中间,展示两侧阴影
|
||||
return '';
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
const listContent = listRef.value?.$el.querySelector('.arco-list-content');
|
||||
if (listContent) {
|
||||
listContent.removeEventListener('scroll', listenScroll);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ms-list {
|
||||
box-shadow: inset 0 10px 6px -10px rgb(0 0 0 / 15%), inset 0 -10px 6px -10px rgb(0 0 0 / 15%);
|
||||
transition: box-shadow 0.1s cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||
.ms-container--shadow();
|
||||
:deep(.arco-list) {
|
||||
@apply rounded-none;
|
||||
.ms-list-item {
|
||||
|
@ -263,13 +230,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.ms-list-hidden-shadow {
|
||||
box-shadow: none;
|
||||
}
|
||||
.ms-list--hidden-top-shadow {
|
||||
box-shadow: inset 0 -10px 6px -10px rgb(0 0 0 / 15%);
|
||||
}
|
||||
.ms-list--hidden-bottom-shadow {
|
||||
box-shadow: inset 0 10px 6px -10px rgb(0 0 0 / 15%);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { ref, useAttrs, watchEffect } from 'vue';
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
|
||||
export type types = 'error' | 'info' | 'success' | 'warning';
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<a-button
|
||||
v-else
|
||||
:class="{
|
||||
'delete': element.danger,
|
||||
'arco-btn-outline--danger': element.danger,
|
||||
'ml-4': true,
|
||||
}"
|
||||
type="outline"
|
||||
|
@ -57,6 +57,7 @@
|
|||
}
|
||||
}
|
||||
.delete {
|
||||
border-color: rgb(var(--danger-6)) !important;
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<template v-if="showProjectSelect">
|
||||
<a-divider direction="vertical" class="ml-0" />
|
||||
<a-select
|
||||
class="w-auto max-w-[200px] focus-within:!bg-[var(--color-text-n8)] hover:!bg-[var(--color-text-n8)]"
|
||||
class="w-auto min-w-[150px] max-w-[200px] focus-within:!bg-[var(--color-text-n8)] hover:!bg-[var(--color-text-n8)]"
|
||||
:default-value="appStore.getCurrentProjectId"
|
||||
:bordered="false"
|
||||
@change="selectProject"
|
||||
|
@ -214,6 +214,7 @@
|
|||
const res = await getProjectList(appStore.getCurrentOrgId);
|
||||
projectList.value = res;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
import { Ref, computed, onBeforeUnmount, ref } from 'vue';
|
||||
|
||||
export interface ContainerShadowOptions {
|
||||
/**
|
||||
* 内容区域
|
||||
*/
|
||||
content?: Ref<HTMLElement>;
|
||||
/**
|
||||
* 判断显示阴影的高度
|
||||
*/
|
||||
overHeight: number;
|
||||
/**
|
||||
* 内容区域的类名
|
||||
*/
|
||||
containerClassName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听指定容器的滚动事件,以给容器添加顶部和地步的阴影,需要给指定的容器添加 .ms-container--shadow() 样式类使用阴影
|
||||
* @param options ContainerShadowOptions
|
||||
*/
|
||||
export default function useContainerShadow(options: ContainerShadowOptions) {
|
||||
const isArrivedTop = ref(true);
|
||||
const isArrivedBottom = ref(true);
|
||||
const isInitListener = ref(false);
|
||||
const containerRef = ref<Ref<HTMLElement> | undefined>(options.content);
|
||||
|
||||
function setContainer(dom: HTMLElement) {
|
||||
if (dom) {
|
||||
containerRef.value = dom;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听列表内容区域滚动,以切换顶部底部阴影
|
||||
* @param event 滚动事件
|
||||
*/
|
||||
function listenScroll(event: Event) {
|
||||
if (event.target) {
|
||||
const listContent = event.target as HTMLElement;
|
||||
const { scrollTop, scrollHeight, clientHeight } = listContent;
|
||||
const scrollBottom = scrollHeight - clientHeight - scrollTop;
|
||||
isArrivedTop.value = scrollTop < options.overHeight;
|
||||
isArrivedBottom.value = scrollBottom < options.overHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function initScrollListener() {
|
||||
if (!isInitListener.value && containerRef.value) {
|
||||
containerRef.value.addEventListener('scroll', listenScroll);
|
||||
isInitListener.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
const containerStatusClass = computed(() => {
|
||||
if (isArrivedTop.value && isArrivedBottom.value) {
|
||||
// 内容不足一屏,不展示阴影
|
||||
return '';
|
||||
}
|
||||
if (isArrivedTop.value) {
|
||||
// 滚动到顶部,隐藏顶部阴影
|
||||
return `${options.containerClassName}-shadow-bottom`;
|
||||
}
|
||||
if (isArrivedBottom.value) {
|
||||
// 滚动到底部,隐藏底部阴影
|
||||
return `${options.containerClassName}-shadow-top`;
|
||||
}
|
||||
// 滚动到中间,展示两侧阴影
|
||||
return `${options.containerClassName}-shadow-top ${options.containerClassName}-shadow-bottom`;
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (containerRef.value) {
|
||||
containerRef.value.removeEventListener('scroll', listenScroll);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
isArrivedTop,
|
||||
isArrivedBottom,
|
||||
isInitListener,
|
||||
containerStatusClass,
|
||||
setContainer,
|
||||
initScrollListener,
|
||||
};
|
||||
}
|
|
@ -3,7 +3,6 @@ import { MENU_LEVEL, pathMap, PathMapItem, PathMapRoute } from '@/config/pathMap
|
|||
import { TreeNode, findNodeByKey, mapTree } from '@/utils';
|
||||
|
||||
export default function usePathMap() {
|
||||
const router = useRouter();
|
||||
/**
|
||||
* 根据菜单级别过滤映射树
|
||||
* @param level 菜单级别
|
||||
|
@ -38,6 +37,7 @@ export default function usePathMap() {
|
|||
* @param openNewPage 是否在新页面打开
|
||||
*/
|
||||
const jumpRouteByMapKey = (key: PathMapRoute, routeQuery?: Record<string, any>, openNewPage = false) => {
|
||||
const router = useRouter();
|
||||
const pathNode = findNodeByKey<PathMapItem>(pathMap, key as unknown as string);
|
||||
if (pathNode) {
|
||||
if (openNewPage) {
|
||||
|
|
|
@ -3,7 +3,7 @@ import ArcoVue from '@arco-design/web-vue';
|
|||
import '@arco-themes/vue-ms-theme-default/index.less';
|
||||
import ArcoVueIcon from '@arco-design/web-vue/es/icon';
|
||||
import SvgIcon from '@/components/pure/svg-icon/index.vue';
|
||||
import MSIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
import { setupI18n } from './locale';
|
||||
|
@ -19,12 +19,12 @@ async function bootstrap() {
|
|||
app.use(store);
|
||||
// 注册国际化,需要异步阻塞,确保语言包加载完毕
|
||||
await setupI18n(app);
|
||||
app.use(router);
|
||||
app.use(ArcoVue);
|
||||
app.use(ArcoVueIcon);
|
||||
app.component('MsIcon', MSIcon);
|
||||
app.component('SvgIcon', SvgIcon);
|
||||
app.component('MsIcon', MsIcon);
|
||||
|
||||
app.use(router);
|
||||
app.use(directive);
|
||||
|
||||
app.mount('#app');
|
||||
|
|
Loading…
Reference in New Issue