分离卡片列表组件.

This commit is contained in:
lijiahang
2023-10-02 22:56:43 +08:00
parent fb11884026
commit 0514ea0508
7 changed files with 478 additions and 380 deletions

View File

@@ -0,0 +1,368 @@
<template>
<div class="card-list-layout">
<!-- 头部部分-固定 -->
<div class="card-list-layout-header" :style="{width: headerWidth}">
<div class="card-list-layout-header-wrapper">
<!-- 信息部分 -->
<div class="card-list-info">
<!-- 路由面包屑 -->
<Breadcrumb />
<!-- 分页部分 -->
<div class="pagination-wrapper">
<a-pagination v-if="pagination"
size="mini"
v-model:current="pagination.current"
v-model:page-size="pagination.pageSize"
v-bind="pagination" />
</div>
</div>
<!-- 操作部分 -->
<div class="card-list-handler">
<!-- 左侧固定 -->
<div class="card-list-handler-left">
<a-space>
<!-- 创建 -->
<div v-if="!handleVisible.disableAdd"
class="header-icon-wrapper"
title="创建"
@click="emits('add')">
<icon-plus />
</div>
</a-space>
<!-- 左侧侧操作槽位 -->
<slot name="leftHandle" />
</div>
<!-- 右侧固定 -->
<div class="card-list-handler-right">
<!-- 右侧操作槽位 -->
<slot name="rightHandle" />
<a-space>
<!-- 搜索框 -->
<div v-if="!handleVisible.disableSearchInput"
class="header-input-wrapper"
:style="{width: searchInputWidth}">
<a-input :default-value="searchValue"
size="small"
placeholder="输入名称/地址"
allow-clear
@input="e => emits('update:searchValue', e)"
@change="e => emits('update:searchValue', e)"
@keydown.enter="emits('search')" />
</div>
<!-- 过滤 -->
<div v-if="!handleVisible.disableFilter"
class="header-icon-wrapper"
title="选择过滤条件">
<icon-filter />
</div>
<!-- 搜索 -->
<div v-if="!handleVisible.disableSearch"
class="header-icon-wrapper"
title="搜索"
@click="emits('search')">
<icon-search />
</div>
<!-- 重置 -->
<div v-if="!handleVisible.disableReset"
class="header-icon-wrapper"
title="重置"
@click="emits('reset')">
<icon-refresh />
</div>
</a-space>
</div>
</div>
</div>
</div>
<!-- 身体部分 -->
<div class="card-list-layout-body">
<!-- 卡片列表 -->
<a-row v-if="list.length !== 0"
:gutter="cardLayoutGutter">
<!-- 添加卡片 -->
<a-col v-if="createCardPosition === 'head'" v-bind="cardLayoutCols">
<create-card :card-height="cardHeight"
:create-card-description="createCardDescription"
@click="emits('add')" />
</a-col>
<!-- 数据卡片 -->
<a-col v-for="(item, index) in list"
:key="item[key]"
v-bind="cardLayoutCols"
:class="{
'disabled-col': item.disabled === true
}">
<a-card :class="{
'general-card': true,
'card-list-item': true,
'card-list-item-disabled': item.disabled === true
}"
:style="{ height: `${cardHeight}px` }"
:body-style="{ height: 'calc(100% - 58px)' }"
:bordered="false"
:hoverable="true">
<!-- 标题 -->
<template #title>
<slot name="title" :record="item" :index="index" :key="item[key]" />
</template>
<!-- 拓展部分 -->
<template #extra>
<slot name="extra" :record="item" :index="index" :key="item[key]" />
</template>
<!-- 内容 -->
<slot name="card" :record="item" :index="index" :key="item[key]" />
</a-card>
</a-col>
<!-- 添加卡片 -->
<a-col v-if="createCardPosition === 'tail'" v-bind="cardLayoutCols">
<create-card :card-height="cardHeight"
:create-card-description="createCardDescription"
@click="emits('add')" />
</a-col>
</a-row>
<!-- 空列表 -->
<template v-else>
<a-card class="general-card empty-list-card"
:style="{ height: `${cardHeight * 2 + 16}px` }"
:body-style="{ height: '100%' }">
<a-empty :class="{
'empty-list-card-body': true,
'empty-list-card-body-creatable': emptyToCreate
}"
:description="emptyDescription"
@click="emits('add')" />
</a-card>
</template>
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'card-list'
};
</script>
<script setup lang="ts">
import { compile, computed, h, PropType } from 'vue';
import { useAppStore } from '@/store';
import { PaginationProps, ResponsiveValue } from '@arco-design/web-vue';
import { Position, CardRecord, ColResponsiveValue, HandleVisible } from './types';
const appStore = useAppStore();
const headerWidth = computed(() => {
const menuWidth = appStore.menu && !appStore.topMenu && !appStore.hideMenu
? appStore.menuCollapse ? 48 : appStore.menuWidth
: 0;
return `calc(100% - ${menuWidth}px)`;
});
const emits = defineEmits(['add', 'update:searchValue', 'search', 'reset']);
defineProps({
key: {
type: String,
default: () => 'id'
},
pagination: {
type: Object as PropType<PaginationProps> | PropType<boolean>,
default: () => false
},
cardHeight: Number,
searchInputWidth: {
type: String,
default: () => '200px'
},
searchValue: {
type: String,
default: () => ''
},
createCardDescription: {
type: String,
default: () => '点击此处进行创建'
},
createCardPosition: {
type: String as PropType<Position>,
default: () => '暂无数据 点击此处进行创建'
},
emptyToCreate: {
type: Boolean,
default: () => true
},
emptyDescription: {
type: String,
default: () => '暂无数据 点击此处进行创建'
},
cardLayoutGutter: {
type: [Number, Array] as PropType<Number> |
PropType<ResponsiveValue> |
PropType<Array<Number>> |
PropType<Array<ResponsiveValue>>,
default: () => [16, 16]
},
cardLayoutCols: {
type: Object as PropType<ColResponsiveValue>,
default: () => {
return {
span: 6
};
}
},
handleVisible: {
type: Object as PropType<HandleVisible>,
default: () => {
return {};
}
},
list: {
type: Array as PropType<Array<CardRecord>>,
default: () => []
},
});
// 创建卡片
const CreateCard = {
props: ['cardHeight', 'createCardDescription'],
setup(props: { cardHeight: any; createCardDescription: any; }) {
return () => {
return h(compile(`
<a-card class="general-card card-list-item create-card"
:style="{ height: '${props.cardHeight}px' }"
:body-style="{ height: '100%' }"
:bordered="false"
:hoverable="true">
<div class="create-card-body">
<icon-plus class="create-card-body-icon" />
<span class="create-card-body-text">${props.createCardDescription}</span>
</div>
</a-card>
`));
};
}
};
</script>
<style scoped lang="less">
@header-info-height: 48px;
@header-handler-height: 48px;
@top-height: 16 + @header-info-height + @header-handler-height + 12px;
.card-list-layout {
&-header {
margin: -16px -16px 0 -16px;
padding: 16px 16px 12px 16px;
position: fixed;
background: var(--color-fill-2);
z-index: 999;
height: @top-height;
transition: none;
&-wrapper {
background: var(--color-bg-4);
padding: 0 12px;
border-radius: 4px;
}
}
&-body {
margin-top: @top-height - 16px;
padding-top: 4px;
}
.disabled-col {
cursor: not-allowed;
.card-list-item-disabled {
pointer-events: none;
opacity: .5;
background: var(--color-bg-1);
}
}
.card-list-info {
height: @header-info-height;
border-bottom: 1px solid var(--color-border-2);
display: flex;
justify-content: space-between;
align-items: center;
}
.card-list-handler {
height: @header-handler-height;
display: flex;
justify-content: space-between;
align-items: center;
&-left {
display: flex;
align-items: center;
}
&-right {
display: flex;
align-items: center;
}
}
}
.header-icon-wrapper {
display: flex;
justify-content: center;
align-items: center;
height: 27px;
padding: 6px;
color: var(--color-text-2);
background: var(--color-fill-2);
border-radius: 2px;
cursor: pointer;
border: 1px solid transparent;
transition: background-color 0.1s cubic-bezier(0, 0, 1, 1);
}
.header-icon-wrapper:hover {
background: var(--color-fill-3);
}
:deep(.create-card) {
&-body {
width: 100%;
height: 100%;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
cursor: pointer;
&-icon {
font-size: 18px;
margin-bottom: 4px;
}
&-text {
user-select: none;
}
}
}
.empty-list-card {
&-body {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding: 0;
}
&-body-creatable {
cursor: pointer;
}
}
</style>

View File

@@ -0,0 +1,35 @@
import { ResponsiveValue } from '@arco-design/web-vue';
/**
* 创建卡片位置
*/
export type Position = 'head' | 'tail' | false
/**
* 卡片字段
*/
export interface CardRecord {
disabled?: boolean;
[name: string]: any;
}
/**
* col 响应式值
*/
export interface ColResponsiveValue extends ResponsiveValue {
span?: number;
offset?: number;
order?: number;
}
/**
* 显示的操作
*/
export interface HandleVisible {
disableAdd?: boolean;
disableSearchInput?: boolean;
disableFilter?: boolean;
disableSearch?: boolean;
disableReset?: boolean;
}

View File

@@ -12,6 +12,7 @@ import {
} from 'echarts/components';
import Chart from './chart/index.vue';
import Breadcrumb from './breadcrumb/index.vue';
import CardList from './card-list/index.vue';
use([
CanvasRenderer,
@@ -31,5 +32,6 @@ export default {
Vue.component('Chart', Chart);
Vue.component('Breadcrumb', Breadcrumb);
Vue.component('a-query-header', AQueryHeader);
Vue.component('card-list', CardList);
},
};

View File

@@ -1,8 +1,7 @@
<script lang="tsx">
import { defineComponent, ref, h, compile, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter, RouteRecordRaw } from 'vue-router';
import { compile, computed, defineComponent, h, ref } from 'vue';
import type { RouteMeta } from 'vue-router';
import { RouteRecordRaw, useRoute, useRouter } from 'vue-router';
import { useAppStore } from '@/store';
import { listenerRouteChange } from '@/utils/route-listener';
import { openWindow, regexUrl } from '@/utils';
@@ -11,7 +10,6 @@
export default defineComponent({
emit: ['collapse'],
setup() {
const { t } = useI18n();
const appStore = useAppStore();
const router = useRouter();
const route = useRoute();

View File

@@ -1,7 +1,9 @@
import { PaginationProps, TableRowSelection } from '@arco-design/web-vue';
import { ColResponsiveValue } from '@/components/card-list/types';
import { reactive } from 'vue';
/**
* FIXME DELETE
* 默认分页
*/
export const defaultPagination = (): PaginationProps => {
@@ -15,6 +17,7 @@ export const defaultPagination = (): PaginationProps => {
};
/**
* FIXME DELETE
* 默认行选择器
*/
export const defaultRowSelection = (): TableRowSelection => {
@@ -25,6 +28,20 @@ export const defaultRowSelection = (): TableRowSelection => {
};
};
/**
* 卡片列表列布局
*/
export const useCardColLayout = (): ColResponsiveValue => {
return {
xs: 24,
sm: 12,
md: 8,
lg: 8,
xl: 6,
xxl: 4,
};
};
/**
* 创建列表分页
*/
@@ -58,9 +75,9 @@ export const useCardPagination = (): PaginationProps => {
* 创建行选择器
*/
export const useRowSelection = (type = 'checkbox'): TableRowSelection => {
return {
return reactive({
type: type as any,
showCheckedAll: true,
onlyCurrent: true,
};
});
};

View File

@@ -1,111 +1,28 @@
<template>
<div class="card-list-layout">
<!-- 头部部分-固定 -->
<div class="card-list-layout-header" :style="{width: headerWidth}">
<div class="card-list-layout-header-wrapper">
<!-- 信息部分 -->
<div class="card-list-info">
<!-- 路由面包屑 -->
<Breadcrumb />
<!-- 分页部分 -->
<div class="pagination-wrapper">
<a-pagination v-if="pagination"
size="mini"
v-model:current="pagination.current"
v-model:page-size="pagination.pageSize"
v-bind="pagination" />
</div>
</div>
<!-- 操作部分 -->
<div class="card-list-handler">
<!-- 左侧固定 -->
<div class="card-list-handler-left">
<a-space>
<!-- 创建 -->
<div class="header-icon-wrapper" title="创建">
<icon-plus />
</div>
<!-- 条件显示 -->
</a-space>
</div>
<!-- 右侧固定 -->
<div class="card-list-handler-right">
<a-space>
<!-- 搜索框 -->
<a-input size="small"
placeholder="输入名称/地址"
allow-clear />
<!-- 过滤 -->
<div class="header-icon-wrapper" title="选择过滤条件">
<icon-filter />
</div>
<!-- 搜索 -->
<div class="header-icon-wrapper" title="搜索">
<icon-search />
</div>
<!-- 重置 -->
<div class="header-icon-wrapper" title="重置">
<icon-refresh />
</div>
</a-space>
</div>
</div>
</div>
</div>
<!-- 身体部分 -->
<div class="card-list-layout-body">
<!-- 卡片列表 -->
<a-row v-if="list.length !== 0"
:gutter="cardLayoutGutter">
<!-- 添加卡片 -->
<a-col v-if="createCardPosition === 'head'" v-bind="cardLayoutCols">
<create-card :card-height="cardHeight" :create-card-description="createCardDescription" />
</a-col>
<!-- 数据卡片 -->
<a-col v-for="(item, index) in list"
:key="item[key]"
v-bind="cardLayoutCols"
:class="{
'disabled-col': item.disabled === true
}">
<a-card :class="{
'general-card': true,
'card-list-item': true,
'card-list-item-disabled': item.disabled === true
}"
:style="{ height: `${cardHeight}px` }"
:body-style="{ height: 'calc(100% - 58px)' }"
:bordered="false"
:hoverable="true">
<!-- 标题 -->
<template #title>
<slot name="title" :record="item" :index="index" :key="item[key]" />
</template>
<!-- 拓展部分 -->
<template #extra>
<slot name="extra" :record="item" :index="index" :key="item[key]" />
</template>
<!-- 内容 -->
<slot name="card" :record="item" :index="index" :key="item[key]" />
</a-card>
</a-col>
<!-- 添加卡片 -->
<a-col v-if="createCardPosition === 'tail'" v-bind="cardLayoutCols">
<create-card :card-height="cardHeight" :create-card-description="createCardDescription" />
</a-col>
</a-row>
<!-- 空列表 -->
<template v-else>
<a-card class="general-card empty-list-card"
:style="{ height: `${cardHeight * 2 + 16}px` }"
:body-style="{ height: '100%' }">
<a-empty :class="{'empty-list-card-body': true, 'empty-list-card-body-creatable': emptyToCreate }"
:description="emptyDescription" />
</a-card>
</template>
</div>
</div>
<card-list ref="cardList"
v-model:searchValue="formModel.name"
create-card-position="head"
:card-height="180"
:list="list"
:pagination="pagination"
:card-layout-cols="cardColLayout"
@add="add"
@search="search"
@reset="reset">
<!-- 标题 -->
<template #title="{ record }">
{{ record.name }}
</template>
<!-- 标题拓展 -->
<template #extra="{ index }">
{{ index }}
</template>
<!-- 卡片 -->
<template #card="{ record }">
{{ record }}
</template>
</card-list>
{{ formModel }}
</template>
<script lang="ts">
@@ -115,248 +32,38 @@
</script>
<script setup lang="ts">
import { compile, computed, h, PropType } from 'vue';
import { useAppStore } from '@/store';
import { PaginationProps, ResponsiveValue } from '@arco-design/web-vue';
import { useCardPagination, useCardColLayout } from '@/types/table';
import { reactive, ref } from 'vue';
const appStore = useAppStore();
const headerWidth = computed(() => {
const menuWidth = appStore.menu && !appStore.topMenu && !appStore.hideMenu
? appStore.menuCollapse ? 48 : appStore.menuWidth
: 0;
return `calc(100% - ${menuWidth}px)`;
const formModel = reactive({
name: undefined
});
export type Position = 'head' | 'tail' | false
const cardColLayout = useCardColLayout();
const pagination = useCardPagination();
const list = ref<Array<any>>([]);
export interface CardList {
disabled?: boolean;
[name: string]: any;
for (let i = 0; i < 5; i++) {
list.value.push({
id: i + 1,
name: `名称 ${i + 1}`,
host: `192.168.1.${i}`,
disabled: i === 0
});
}
defineProps({
key: {
type: String,
default: () => 'id'
},
pagination: {
type: Object as PropType<PaginationProps> | PropType<boolean>,
default: () => false
},
cardHeight: Number,
createCardDescription: {
type: String,
default: () => '点击此处进行创建'
},
emptyToCreate: {
type: Boolean,
default: () => true
},
emptyDescription: {
type: String,
default: () => '暂无数据 点击此处进行创建'
},
createCardPosition: {
type: String as PropType<Position>,
default: () => '暂无数据 点击此处进行创建'
},
cardLayoutGutter: {
type: [Number, Array] as PropType<Number> |
PropType<ResponsiveValue> |
PropType<Array<Number>> |
PropType<Array<ResponsiveValue>>,
default: () => [16, 16]
},
cardLayoutCols: {
type: Object as PropType<ResponsiveValue & { span?: number, offset?: number, order?: number }>,
default: () => {
return {
span: 6
};
}
},
list: {
type: Array as PropType<Array<CardList>>,
default: () => []
},
});
// props
// const pagination = useCardPagination();
// const cardHeight = 120;
// const createCardDescription = '点击此处进行创建';
// const emptyToCreate = true;
// const emptyDescription = '暂无数据 点击此处进行创建';
// // head tail false
// const createCardPosition = 'head';
// const cardLayoutGutter = [
// { xs: 16, sm: 16, md: 16, lg: 16, xl: 16, xxl: 16 },
// { xs: 16, sm: 16, md: 16, lg: 16, xl: 16, xxl: 16 }
// ];
// const cardLayoutCols = {
// xs: 24,
// sm: 12,
// md: 8,
// lg: 8,
// xl: 6,
// xxl: 4,
// };
// // const cardHeight = 120;
// // const pagination = useCardPagination();
// const list = ref<Array<any>>([]);
//
// for (let i = 0; i < 270; i++) {
// list.value.push({
// id: i + 1,
// name: `名称 ${i + 1}`,
// host: `192.168.1.${i}`
// });
// }
// pagination.total = 270;
// 创建卡片
const CreateCard = {
props: ['cardHeight', 'createCardDescription'],
setup(props: { cardHeight: any; createCardDescription: any; }) {
return () => {
return h(compile(`
<a-card class="general-card card-list-item create-card"
:style="{ height: '${props.cardHeight}px' }"
:body-style="{ height: '100%' }"
:bordered="false"
:hoverable="true">
<div class="create-card-body">
<icon-plus class="create-card-body-icon" />
<span class="create-card-body-text">${props.createCardDescription}</span>
</div>
</a-card>
`));
};
}
pagination.total = 270;
const add = () => {
console.log('add');
};
const search = () => {
console.log('search');
};
const reset = () => {
console.log('reset');
};
</script>
<style scoped lang="less">
@header-info-height: 48px;
@header-handler-height: 48px;
@top-height: 16 + @header-info-height + @header-handler-height + 12px;
.card-list-layout {
&-header {
margin: -16px -16px 0 -16px;
padding: 16px 16px 12px 16px;
position: fixed;
background: var(--color-fill-2);
z-index: 999;
height: @top-height;
transition: none;
&-wrapper {
background: var(--color-bg-4);
padding: 0 12px;
border-radius: 4px;
}
}
&-body {
margin-top: @top-height - 16px;
padding-top: 4px;
}
.disabled-col {
cursor: not-allowed;
.card-list-item-disabled {
pointer-events: none;
opacity: .5;
background: var(--color-bg-1);
}
}
.card-list-info {
height: @header-info-height;
border-bottom: 1px solid var(--color-border-2);
display: flex;
justify-content: space-between;
align-items: center;
}
.card-list-handler {
height: @header-handler-height;
display: flex;
justify-content: space-between;
align-items: center;
&-left {
display: flex;
align-items: center;
}
&-right {
display: flex;
align-items: center;
}
}
}
.header-icon-wrapper {
display: flex;
justify-content: center;
align-items: center;
height: 27px;
padding: 6px;
color: var(--color-text-2);
background: var(--color-fill-2);
border-radius: 2px;
cursor: pointer;
border: 1px solid transparent;
transition: background-color 0.1s cubic-bezier(0, 0, 1, 1);
}
.header-icon-wrapper:hover {
background: var(--color-fill-3);
}
:deep(.create-card) {
&-body {
width: 100%;
height: 100%;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
cursor: pointer;
&-icon {
font-size: 18px;
margin-bottom: 4px;
}
&-text {
user-select: none;
}
}
}
.empty-list-card {
&-body {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding: 0;
}
&-body-creatable {
cursor: pointer;
}
}
</style>

View File

@@ -8,25 +8,10 @@
@openUpdateConfig="(e) => config.open(e)" />
<!-- 列表-卡片 -->
<host-card-list v-else
:create-card-position="'head'"
:card-height="180"
:list="list"
:pagination="pagination"
ref="card"
@openAdd="() => modal.openAdd()"
@openUpdate="(e) => modal.openUpdate(e)"
@openUpdateConfig="(e) => config.open(e)">
<template #title="{ record }">
{{ record.name }}
</template>
<template #extra="{ index }">
{{ index }}
</template>
<template #card="{ record }">
{{ record }}
</template>
</host-card-list>
ref="card" />
<!-- @openAdd="() => modal.openAdd()"-->
<!-- @openUpdate="(e) => modal.openUpdate(e)"-->
<!-- @openUpdateConfig="(e) => config.open(e)" />-->
<!-- 添加修改模态框 -->
<host-form-modal ref="modal"
@added="() => table.addedCallback()"
@@ -49,20 +34,6 @@
import HostCardList from '@/views/asset/host/components/host-card-list.vue';
import HostFormModal from './components/host-form-modal.vue';
import HostConfigDrawer from '@/views/asset/host/components/host-config-drawer.vue';
import { useCardPagination } from '@/types/table';
const pagination = useCardPagination();
const list = ref<Array<any>>([]);
for (let i = 0; i < 270; i++) {
list.value.push({
id: i + 1,
name: `名称 ${i + 1}`,
host: `192.168.1.${i}`,
disabled: i === 0
});
}
pagination.total = 270;
const table = ref();
const card = ref();