предисловие
Я был в Тибете в Национальный день, пейзажи там очень хорошие👍, но у меня каждый день болит голова😣, я не могу не чувствовать, что лучше написать статью✍, Итак, сегодня я хочу поделиться с вами реализация табличного компонента, который находится от 0 до 1 О, этот компонент должен быть для нас довольно сложным.Посмотрев столько первичных компонентов, пришло время установить форк 😏.
Обзор знаний
С формой мы наверняка соприкоснулись, особенно при разработке системы управления бэкграундом, но большинство из них пишется напрямую с UI framework, как писалось давно не знаю, поэтому сначала рассмотрим. 🤔 Базовое письмо самой примитивной формы:
<table border="1">
<colgroup>
<!-- 这里可以针对每列做一些属性设置,例如设置宽度,这个我还真不知道,也许曾经看过但忘了 -->
<col width="200">
<col width="150">
<col width="100">
</colgroup>
<thead>
<tr>
<th>姓名</th>
<th>职位</th>
<th>等级</th>
</tr>
</thead>
<tbody>
<tr>
<td>尤水就下</td>
<td>前端</td>
<td>小菜</td>
</tr>
</tbody>
</table>
Приведенная выше таблица выглядит примерно так:Среди них следует отметить, что
<col>потому что эта вещь мне совершенно чужда (на самом деле я не знаю
😂), а потом поискал, оказывается, нам удобно задавать какие-то общие свойства для каждой колонки, например ширину. Потому что мы будем использовать его позже, поэтому, пожалуйста, помните 👀.
Цель
Сначала кратко расскажем о том, чего мы хотим добиться в этой статье: базовое отображение + выбрать все + сортировка + расширение + настраиваемый контент + фиксированный заголовок + (многоуровневый заголовок + фиксированный столбец). Тогда мне нечего сказать, просто закатаю рукава и сделаю это 💪.
Основной дисплей
В виде таблицы отображение данных является необходимой функцией.Конечно, ее легко реализовать, но более важным является дизайн API.Хороший API может заставить вас делать больше с меньшими затратами, поэтому, изучив API основных фреймворков, мы надеемся, что разработка использует наши компоненты следующим образом👇:
<template>
<div id="app">
<xr-table :columns="columns" :data="data"></xr-table>
</div>
</template>
<script>
import XrTable from './components/xr-table';
export default {
components: {
XrTable
},
data() {
return {
columns: [
{
title: '姓名',
key: 'name'
},
{
title: '年龄',
key: 'age'
},
{
title: '职位',
key: 'job'
}
],
data: [
{
id: 1,
name: 'Jasmine',
age: 18,
job: '产品',
desc: '这是展开的描述啊1'
},
{
id: 2,
name: 'Mango',
age: 18,
job: '设计',
desc: '这是展开的描述啊2'
},
{
id: 3,
name: 'Aking',
age: 24,
job: '前端',
desc: '这是展开的描述啊3'
},
{
id: 4,
name: 'Dick',
age: 30,
job: '后端',
desc: '这是展开的描述啊4'
},
{
id: 5,
name: 'Lucy',
age: 18,
job: '测试',
desc: '这是展开的描述啊5'
}
]
};
}
};
</script>
То есть вы несете ответственность за предоставление данных (columnsтребуют иметьkey,dataтребоватьid), буду рендерить, эта часть на самом деле не сложная, и очень красиво с небольшой таблицей стилей, вот непосредственно код:
<template>
<div class="xr-table">
<table>
<thead>
<tr>
<!-- 表头循环 -->
<th v-for="col in columns" :key="col.key">{{col.title}}</th>
</tr>
</thead>
<tbody>
<!-- 表体循环 -->
<tr v-for="row in data" :key="row.id">
<td v-for="col in columns" :key="col.key">{{row[col.key]}}</td>
</tr>
</tbody>
</table>
</div>
</template>
<style lang="scss">
.xr-table {
table {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
empty-cells: show;
border: 1px solid #e9e9e9;
}
table th {
background: #f7f7f7;
color: #5c6b77;
font-weight: 600;
white-space: nowrap;
}
table td,
table th {
padding: 8px 16px;
border: 1px solid #e9e9e9;
text-align: left;
}
}
</style>
Фактически,thа такжеtdПовторите еще раз, не нужно слишком много объяснять 😁. Затем запустите код и посмотрите на результат:Таким образом, у простой таблицы, конечно, есть нечто большее.
выбрать все
Далее нам нужно добавить функцию select-all в таблицу. Во-первых, давайте посмотрим на API. После ссылки мы можем пройти в входящемcolumnsПоднимите в нем шум и вызовитеon-selection-changeСобытия, подобные следующим 👇:
<template>
<xr-table :columns="columns" :data="data" @on-selection-change="onSelectionChange"></xr-table>
</template>
<script>
export default {
data() {
return {
columns: [
{
type: 'selection' // 这个地方可以不用写 key,type 就相当于 key
}
...
]
}
}
}
</script>
Честно говоря, я думаю, что этот дизайн API довольно умный, и нам не нужно писать как Element:
<el-table>
<el-table-column
type="selection"
width="55">
</el-table-column>
</el-table>
Тогда как изменить компонент? Тоже очень просто, в циклеthа такжеtdсначала определите, есть лиtype,Если естьtypeи значениеselectionОн отображается в виде флажка, как показано ниже👇:
<template>
<div class="xr-table">
<table>
<thead>
<tr>
<th v-for="col in columns" :key="col.key">
<div>
<!-- 在这里先判断 type -->
<template v-if="col.type === 'selection'">
<input ref="allCheckbox" type="checkbox" :checked="isSelectAll" @change="selectAll">
</template>
<template v-else>{{col.title}}</template>
</div>
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in data" :key="row.id">
<td v-for="col in columns" :key="col.key">
<div>
<!-- 在这里先判断 type -->
<template v-if="col.type === 'selection'">
<input
type="checkbox"
:checked="formateStatus(row)"
@change="toggleSelect($event, row)"
>
</template>
<template v-else>{{row[col.key]}}</template>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</template>
Конечно, приведенный выше код просто отображает флажок, и теперь нам нужно добавить к нему логику щелчка. Здесь мы поддерживаем компонент в компонентеselectedRowsполе, мы помещаем текущие выбранные строки в этот массив, и каждый раз, когда флажок установлен, он будет измененselectedRowsценность . Обратите внимание, что значение флажка заголовкаisSelectAllосновывается на всех данныхdataи выбранные строкиselectedRowsдля сравнения, такisSelectAllЭто должно быть записано в вычисляемом свойстве, например:
<script>
export default {
...
data() {
return {
selectedRows: [] // 当前已选中的行
};
},
computed: {
isSelectAll() { // 表头全选的勾选状态应该根据当前已选的来计算,最好不要直接比较数组长度是否相等,而是应该在比较长度的基础上比较每一项的 id 是否一样,虽然目前看起来这个步骤很多余
let all = this.data.map(item => item.id).sort();
let selected = this.selectedRows.map(item => item.id).sort();
let isSelectAll = true;
if (all.length === selected.length) {
for (let i = 0, len = all.length; i < len; i++) {
if (all[i] !== selected[i]) {
isSelectAll = false;
break;
}
}
} else {
isSelectAll = false;
}
this.$nextTick(() => { // 这个是选了部分之后把表头的复选框改变成中间状态(就是横杠的状态)
this.$refs['allCheckbox'][0].indeterminate =
selected.length && !isSelectAll;
});
return isSelectAll;
}
},
methods: {
selectAll(e) { // 单击表头的多选框并向外触发事件
let checked = e.target.checked;
this.selectedRows = checked ? JSON.parse(JSON.stringify(this.data)) : [];
this.$emit('on-selection-change', this.selectedRows);
},
toggleSelect(e, row) { // 单击表体的多选框并向外触发事件
let checked = e.target.checked;
if (checked) {
this.selectedRows.push(row);
} else {
let idx = this.selectedRows.findIndex(item => item.id === row.id);
this.selectedRows.splice(idx, 1);
}
this.$emit(
'on-selection-change',
JSON.parse(JSON.stringify(this.selectedRows))
);
},
formateStatus(row) { // 表体的每个多选框是否被勾选
return this.selectedRows.findIndex(item => item.id === row.id) >= 0;
}
}
};
</script>
Комментарии в приведенном выше коде должны объяснить это достаточно ясно.Давайте посмотрим на результат реализации:Все должно быть в порядке!
Сортировать
Теперь нам нужно добавить в таблицу функцию сортировки. Давайте сначала посмотрим на дизайн API. По сути, он похож на функцию выбора всего. Мы все еще находимся вcolumnsПоднимите шумиху внутри, а потом поддержите внешнее срабатываниеon-sortСобытие может быть следующим👇:
<template>
<div id="app">
<xr-table
:columns="columns"
:data="data"
@on-sort="onSort"
></xr-table>
</div>
</template>
<script>
export default {
...
data() {
return {
columns: [
...
{
title: '年龄',
key: 'age',
sortable: true
}
...
]
}
}
}
Однако здесь мы не выполняем операции сортировки на самом деле😬, потому что сортировкой должен заниматься бэкенд, а за срабатывание событий, вызов интерфейсов, получение данных и обновление таблиц отвечает фронтенд, ведь простая сортировка во фронтенде не нужна. не имеет большого смысла. Тогда когда может понадобиться фронтальная сортировка, то есть объем данных не большой.Если фронтенд получает все данные за один раз, это может сэкономить время вызова интерфейса, но если это может быть переместил в серверную часть, давайте подтолкнем его. Давайте посмотрим. Код:
...
<template v-if="col.type === 'selection'">
<input ref="allCheckbox" type="checkbox" :checked="isSelectAll" @change="selectAll">
</template>
<template v-else>
<!-- 改动在这里 -->
<span>{{col.title}}</span>
<span v-if="col.sortable">
<i @click="handleSort(col.key, 'asc')">↑</i>
<i @click="handleSort(col.key, 'desc')">↓</i>
</span>
</template>
...
<script>
export default {
...
methods: {
handleSort(key, sortType) {
this.$emit('on-sort', { key, sortType });
}
}
}
</script>
Хотя это очень просто, основная цель написания здесь сортировки — позволить нашей таблице поддерживать добавление некоторых значков и пользовательских событий в заголовок таблицы.Результаты следующие:Тоже нормально!
расширять
Расширение на самом деле то же самое, что и две вышеупомянутые функции, API должен быть вcolumnsСделайте в нем статью:
<script>
export default {
...
data() {
return {
columns: [
{
type: 'expand'
}
...
]
}
}
}
Здесь мы намерены поддерживатьexpandIds, сохраняет информацию обо всех развернутых строках и выбирает всеselectedRowsТе же волосы. Но на самом деле может быть и другой путь: то есть мы обрабатываем поступающие данные заранее, вdataКаждая строка внутри добавляетisExpandа такжеisSelectполе, а затем изменить соответствующее состояние во время работы, и нет необходимости объявлять дополнительный массив. Но меня не волнует, как этого добиться, это последнее слово, чтобы это сделать, поэтому, пожалуйста, смотрите следующий код👇:
...
<template v-if="col.type === 'selection'">
<input ref="allCheckbox" type="checkbox" :checked="isSelectAll" @change="selectAll">
</template>
<!-- 改动在这里 -->
<template v-else-if="col.type === 'expand'"></template>
...
<tbody>
<template v-for="row in data">
<tr :key="row.id">
<td v-for="col in columns" :key="col.key">
...
</td>
</tr>
<!-- 这里多加了一个是否展开的判断 -->
<tr :key="`expand-${row.id}`" v-if="checkIsExpand(row.id)">
<!-- 横跨所有列 -->
<td :colspan="columns.length">{{row.desc}}</td>
</tr>
</template>
</tbody>
...
<script>
export default {
...
data() {
return {
expandIds: []
};
},
methods: {
...
toggleExpand(id) {
let idx = this.expandIds.indexOf(id);
if (idx >= 0) {
this.expandIds.splice(idx, 1);
} else {
this.expandIds.push(id);
}
},
checkIsExpand(id) {
return this.expandIds.indexOf(id) >= 0;
}
}
}
</script>
Следует отметить, что расширенное содержимое должно охватывать все строки, поэтому мы должны поставитьcolspanустановлено значениеcolumns.length, что означает по всем столбцам. Конечно, сейчас мы пишем содержимое расширенного поля.desc, мы будем поддерживать настройку через некоторое время. На стиль здесь не обращаем внимания, ведь это не главное, тогда запускайте и смотрите эффект👇:Кажется интересным!
пользовательский контент
Поговорим об этом👏, на самом деле мы уже поддержали самые основные потребности формы, но проблема в том, что текущая форма слишком базовая, что если я захочу настроить контент (трижды почешу затылок 😧), поддержите его в любом случае . Значит, дальше нам нужно это сделать!
Во-первых, даже не думайте об этом, эта штука должна использовать слот, но нам все равно нужно начать с дизайна API Теперь предположим, что мы хотим добавить слово «годы» в конец столбца возраста. , и добавить редактор в последнюю колонку, и удалить две кнопки, мы должны ожидать такого использования👇:
<template>
<xr-table
:columns="columns"
:data="data">
<!-- 其中 age 是对应的插槽名,{row, col, index} 对应的是行、列、索引这三个参数 -->
<template v-slot:age="{row, col, index}">{{ row.age + '岁'}}</template>
<template v-slot:action="{row, col, index}">
<button>编辑{{index}}</button>
<button>删除</button>
</template>
</xr-table>
</template>
<script>
export default {
...
data() {
return {
columns: [
...
{
title: '年龄',
slot: 'age', // 写了 slot 也可以不用写 key,因为它相当于 key
sortable: true
},
...
{
title: '操作',
slot: 'action'
}
]
}
...
}
мы вcolumnsдобавилslotполе, которое используется, чтобы указать, нуждается ли столбец таблицы в пользовательском содержимом, а затем<xr-table></xr-table>Внутри написано а<template v-slot:age="{row, col, index}">{{ index }}</template>такое дело, в которомageэто соответствующее имя слота,{row, col, index}В соответствии с тремя параметрами строки, столбца и индекса, я не знаю, знакомы ли вы с этим, на самом деле этоslot-scopeНовый способ написания, давайте ознакомимся с инструкцией на официальном сайте (для тех, кто не знает, можете зайти посмотреть, как им пользоваться, это не сложно):
v-slotСлоты — штука действительно полезная, писать кастомный контент было довольно хлопотно.v-slotУпрощает реализацию, а также имеет сокращение, которое можно использовать с#заменятьv-slot, подобно@заменятьv-onТо же самое, то есть<template #age="slotProps">{{ index }}</template>.
Хорошо, а теперь посмотрим, как написать код в компоненте, по сути нам нужно только исправитьtbodyВы можете вносить в него изменения, это не так сложно, как вы думаете, ведь изменения не большие, поэтому вот непосредственно код:
<tbody>
<template v-for="(row, index) in data">
<tr :key="row.id">
<td v-for="col in columns" :key="col.key">
<div>
<!-- 改动在这里,我们我先判断列是否有 slot 字段 -->
<template v-if="col.slot">
<!-- row,col,index 是我们需要主动传出去的参数,这样在外面的 slotProps 才能拥有这几个参数,当然我们还可以传其他参数,他们最终都会被放在 slotProps 这一个对象里面 -->
<slot :name="col.slot" :row="row" :col="col" :index="index"></slot>
</template>
<template v-else-if="col.type === 'selection'">
...
</template>
...
</div>
</td>
</tr>
...
</template>
</tbody>
Окончательный рендеринг — это то, что мы хотим, как показано на следующем рисунке👇:По этой же причине мы также можем немного модифицировать упражнение расширения для поддержки кастомизации.Вы можете попробовать, и мы не будем его здесь показывать 😁.
фиксированный заголовок
Так называемый фиксированный заголовок означает, что заголовок не двигается, но тело можно прокручивать, это немного хлопотно реализовать🤔, почему? Потому что изначально наша таблица оказаласьtheadа такжеtbodyОн состоит из двух частей.tbodyДайте высоту и пусть она переполняется и прокручивается, но все не так просто,heightа такжеoverflowЭти два стиля написаны наtbodyилиtableЭто деревянный эффект, поэтому мы должны посмотреть, сделают ли люди (предпринимать элемент):На изображении выше мы ясно видим
theadа такжеtbodyбыли размещены в двух разныхdivизtableвнутри, затем установитеtbodyвнешний слойdivПросто прокрутка. Что ж, честные и прямые мысли 😯. На самом деле остальные основные UI-фреймворки одинаковы (то есть написаны отдельно). Так что раз люди уже это практиковали, значит, решение выполнимо, по крайней мере, совместимость реализована, поэтому мы можем начать писать свой собственный код после того, как соберем сильные стороны сотен школ. Конечно, первый шаг — это дизайн API, на этот раз нам нужно только передать тегheightПараметры могут быть:
<xr-table
:columns="columns"
:data="data"
height="150"
></xr-table>
Далее нам нужно изменить внутреннюю структуру компонента и преобразовать его в метод записи из двух частей. На самом деле формаtheadа такжеtbodyОн изначально был разделен, так что разобрать его не сложно, как и следующее👇:
<template>
<!-- 这里只是单纯改了结构,里面的 tr 并没有改变 -->
<div class="xr-table">
<div class="xr-table__header">
<table>
<thead>...</thead>
</table>
</div>
<div class="xr-table__body">
<table>
<tbody>..</tbody>
</table>
</div>
</div>
</template>
Сохраните код, текущий рендеринг выглядит следующим образом:Ну отличий от оригинала нет, но возникает первый вопрос,
theadширина иtbodyШирина не выровнена, как это можно настроить. Что еще можно сделать, просто выставить ширину 😳, пройтиcolumnsпройти в каждой колонкеwidthЗначение в порядке (конечно, может быть один столбец, которому не нужно указывать ширину, чтобы он мог сохранять гибкость), а затем поместитеtheadа такжеtbodyВы можете установить для каждого столбца одинаковую ширину.Возможно, это будет хлопотно, но на самом деле это позволит избежать некоторых других ненужных проблем.Вот я вырезал картинки с двух официальных сайтов Ant Design и Element:
Хорошо видно, что они тоже нужны
width, так что здесь мы должны датьcolumnsдобавить еще одно полеwidth, так{title: '姓名', key: 'name', width: 100}, где единица измерения по умолчаниюpx. Тогда как установить ширину? Ха-ха 😁, здесь мы будем использовать то, что упоминали в начале<colgroup>внутри<col>Что ж, эта штука действительно в самый раз для настройки ширины. В компоненте не так много мест для изменения, просто добавьте ширину, как показано ниже👇:
<div class="xr-table__header">
<table>
<colgroup>
<col v-for="col in columns" :width="col.width || ''">
</colgroup>
...
</table>
</div>
<div class="xr-table__body">
<table>
<colgroup>
<col v-for="col in columns" :width="col.width || ''">
</colgroup>
...
</table>
</div>
Здесь мы оставляем ширину предпоследнего столбца пустой и видим эффект:Ну вроде неплохо 👏, продолжим. Следующее, что нужно сделать, это установить высоту тела и свернуть его. Сначала мы прошли в
heightЭто должна быть высота всей таблицы, поэтому она должна быть в самом дальнем конце.xr-tableдобавитьheight: 150px; overflow: hiddenстиль, то нужно рассчитать и установитьxr-table__bodyвысота (общая высота - заголовок) иoverflowvalue, чтобы тело таблицы можно было прокручивать. Глядя на код ниже, на самом деле изменений немного:
<template>
<!-- 加了个 tableStyle -->
<div class="xr-table" :style="tableStyle">
<!-- 加了个 ref -->
<div class="xr-table__header" ref="tableHeader">...</div>
<div class="xr-table__body" ref="tableBody">...</div>
</div>
</template>
<script>
export default {
...
mounted() {
let { tableHeader, tableBody } = this.$refs;
let headerH = parseInt(window.getComputedStyle(tableHeader).height);
let bodyH = this.height - headerH;
tableBody.style.height = `${bodyH}px`;
},
computed: {
tableStyle() {
return this.height ? `height: ${this.height}px` : '';
}
}
...
}
</script>
<style lang="scss">
.xr-table {
overflow: hidden;
&__body {
overflow: auto;
}
}
</style>
Сохраняем и запускаем, эффект такой:Что ж, неплохо, хотя стиль немного ущербный, но это не беда, ведь функция реализована✌. Если вам нужно увидеть исходный код, вы можете нажать здесь:табличный компонент.
шесть шесть шесть, великая похвала👍👍👍
Эпилог
Это может быть слишком много, чтобы писать здесь, поэтому мы оставим многоуровневый заголовок и фиксированные столбцы для следующей главы и надеемся хорошо написать во второй половине этого месяца😂. Хотя то, что я написал относительно просто, это процесс с 0. Надеюсь, это может быть полезно для всех, до новых встреч👋.