feat(组件): 缩略卡片组件&卡片列表业务组件&部分组件调整&useContainerShadow钩子

This commit is contained in:
baiqi 2023-09-25 11:21:45 +08:00 committed by 刘瑞斌
parent eba0879538
commit eed7fdb0b2
17 changed files with 695 additions and 214 deletions

View File

@ -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",

View File

@ -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();

View File

@ -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);
}
}
}
}

View File

@ -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%);
}
}

View File

@ -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'; // listapi
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>

View File

@ -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;

View File

@ -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'; // hoverhover hover footerdefault 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>

View File

@ -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));
}
}
}
}

View File

@ -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;

View File

@ -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>

View File

@ -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';

View File

@ -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<{

View File

@ -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>

View File

@ -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);
}
});

View File

@ -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,
};
}

View File

@ -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) {

View File

@ -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');