Last active
August 4, 2024 15:55
-
-
Save Hogeyama/b7821b6c87a19d5b9fe55e1d82ba12aa to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <script setup lang="ts" generic="T"> | |
| import { reactive, ref } from "vue"; | |
| import type { Row, TableDef } from "./types"; | |
| import { AdjustmentsHorizontalIcon, XMarkIcon } from "@heroicons/vue/24/solid"; | |
| const { columns, body, rowError, colError } = defineProps<TableDef<T>>(); | |
| // FIXME 効率悪い | |
| const hasAnyError = (row: Row<T>) => { | |
| if (rowError(row)) return true; | |
| for (const column of columns) { | |
| if (colError(row, column)) return true; | |
| } | |
| return false; | |
| }; | |
| const config_open = ref(true); | |
| const toggleConfig = () => { | |
| config_open.value = !config_open.value; | |
| }; | |
| const sort_option = reactive({ | |
| order: "asc", | |
| column: "index", | |
| }); | |
| type FilterItem = { | |
| index: number; | |
| column: string; | |
| value: string; | |
| }; | |
| const filter_option = ref<FilterItem[]>([ | |
| { | |
| index: 0, | |
| column: "", | |
| value: "", | |
| }, | |
| ]); | |
| const is_first_filter = (filter: FilterItem) => { | |
| return filter.index === 0; | |
| }; | |
| const is_last_filter = (filter: FilterItem) => { | |
| return filter.index === filter_option.value.length - 1; | |
| }; | |
| const addEmptyFilterIfNeeded = () => { | |
| console.log("adjust filter number"); | |
| let all_filed = true; | |
| for (const filter of filter_option.value) { | |
| if (filter.column === "" || filter.value === "") { | |
| all_filed = false; | |
| break; | |
| } | |
| } | |
| if (all_filed) { | |
| const index = filter_option.value.length; | |
| filter_option.value = [ | |
| ...filter_option.value, | |
| { | |
| index, | |
| column: "", | |
| value: "", | |
| }, | |
| ]; | |
| } | |
| }; | |
| const deleteFilter = (filter: FilterItem) => { | |
| filter_option.value = filter_option.value | |
| .filter((f) => f.index !== filter.index) | |
| .map((f, i) => ({ ...f, index: i > filter.index ? i - 1 : i })); | |
| }; | |
| const updateTableConfig = () => { | |
| console.log({ | |
| sort_option: JSON.stringify(sort_option), | |
| filter_option: JSON.stringify(filter_option.value), | |
| }); | |
| config_open.value = false; | |
| }; | |
| // * カラムの表示トグル | |
| // * ページ送り | |
| </script> | |
| <!-- eslint-disable vue/html-self-closing --> | |
| <template> | |
| <div :class="['rounded-md border border-gray-300']"> | |
| <div :class="['flex flex-row items-center justify-between']"> | |
| <div :class="['ml-2 mt-2 font-medium']"> | |
| {{ "タイトル" }} | |
| </div> | |
| <button class="mr-2 mt-2" @click="toggleConfig"> | |
| <AdjustmentsHorizontalIcon :class="'size-4 '" /> | |
| </button> | |
| </div> | |
| <!-- | |
| これは本来テーブルの外の役目な気もする。 | |
| とりあえずはここに置いておく。 | |
| --> | |
| <div :class="{ collapse: true, 'collapse-open': config_open }"> | |
| <form | |
| class="collapse-content border px-4 text-sm" | |
| @submit.prevent="updateTableConfig" | |
| > | |
| <div | |
| :class="[ | |
| 'pt-2', | |
| 'grid auto-cols-auto grid-cols-5 place-content-center', | |
| 'w-full', | |
| ]" | |
| > | |
| <div class="p-2">並び替え</div> | |
| <div class="px-2"> | |
| <select | |
| v-model="sort_option.column" | |
| class="select select-bordered select-sm w-full" | |
| > | |
| <option :value="'index'">行</option> | |
| <option | |
| v-for="column in columns" | |
| :key="column.name" | |
| :value="column.name" | |
| > | |
| {{ column.label }} | |
| </option> | |
| </select> | |
| </div> | |
| <div class="px-2"> | |
| <select | |
| v-model="sort_option.order" | |
| class="select select-bordered select-sm min-w-20 px-2" | |
| > | |
| <option value="asc">昇順</option> | |
| <option value="desc">降順</option> | |
| </select> | |
| </div> | |
| <div /> | |
| <div /> | |
| <template v-for="filter in filter_option" :key="filter.column"> | |
| <div :class="{ 'p-2': true, invisible: !is_first_filter(filter) }"> | |
| フィルター | |
| </div> | |
| <div class="px-2"> | |
| <select | |
| :value="filter.column" | |
| class="select select-bordered select-sm w-full" | |
| @change=" | |
| (e: any) => { | |
| filter.column = e.target?.value; | |
| addEmptyFilterIfNeeded(); | |
| } | |
| " | |
| > | |
| <option :value="'index'">行</option> | |
| <option | |
| v-for="column in columns" | |
| :key="column.name" | |
| :value="column.name" | |
| > | |
| {{ column.label }} | |
| </option> | |
| </select> | |
| </div> | |
| <div class="col-span-2 px-2"> | |
| <input | |
| type="text" | |
| class="input input-sm input-bordered w-full" | |
| @blur=" | |
| (e: any) => { | |
| filter.value = e.target?.value; | |
| addEmptyFilterIfNeeded(); | |
| } | |
| " | |
| /> | |
| </div> | |
| <div> | |
| <button v-if="is_last_filter(filter)" class="btn btn-sm w-20"> | |
| {{ "更新" }} | |
| </button> | |
| <button v-else @click="() => deleteFilter(filter)"> | |
| <XMarkIcon class="p-2" /> | |
| </button> | |
| </div> | |
| </template> | |
| </div> | |
| </form> | |
| </div> | |
| <div :class="['overflow-auto', 'max-h-80']"> | |
| <table class="table table-pin-rows table-pin-cols table-xs my-0"> | |
| <thead class="border-0"> | |
| <!-- この h-8 と header tbody の top-8 は同じ高さを指している --> | |
| <tr class="z-30 h-8 border-0"> | |
| <th class="z-30 min-w-20" scope="col">行</th> | |
| <td | |
| v-for="column in columns" | |
| :key="column.name" | |
| class="z-10 min-w-40" | |
| scope="col" | |
| > | |
| <div class="flex items-center"> | |
| {{ column.label }} | |
| <div class="group relative flex cursor-help items-center"> | |
| <span class="ml-1 text-gray-400 group-hover:text-gray-600"> | |
| <svg | |
| xmlns="http://www.w3.org/2000/svg" | |
| class="size-4" | |
| fill="none" | |
| viewBox="0 0 24 24" | |
| stroke="currentColor" | |
| > | |
| <path | |
| stroke-linecap="round" | |
| stroke-linejoin="round" | |
| stroke-width="2" | |
| d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" | |
| /> | |
| </svg> | |
| </span> | |
| <div | |
| :class="[ | |
| 'invisible group-hover:visible', | |
| 'opacity-100 group-hover:opacity-100', | |
| 'transition-opacity duration-300', | |
| 'absolute', | |
| 'bg-white', | |
| 'z-50', | |
| 'rounded text-xs text-gray-700', | |
| 'left-1/2 top-full -translate-x-1/2 px-3 ', | |
| ]" | |
| > | |
| <component :is="column.explanation" /> | |
| </div> | |
| </div> | |
| </div> | |
| </td> | |
| </tr> | |
| </thead> | |
| <template v-for="section in body" :key="section.key"> | |
| <tbody v-if="section.sep !== undefined"> | |
| <!-- この top-8 と header の h-8 は同じ高さを指している --> | |
| <tr class="sticky top-8 z-20"> | |
| <th | |
| :colspan="columns.length" | |
| class="sticky inset-x-0 z-20 min-w-20 bg-blue-100 text-left" | |
| > | |
| <div class="ml-4">{{ section.sep }}</div> | |
| </th> | |
| </tr> | |
| </tbody> | |
| <tbody> | |
| <tr v-for="row in section.rows" :key="row.index"> | |
| <th | |
| :class="[ | |
| 'z-10 whitespace-nowrap text-center', | |
| hasAnyError(row) ? 'bg-red-100' : 'bg-blue-50', | |
| ]" | |
| > | |
| {{ row.index }} | |
| </th> | |
| <td | |
| v-if="rowError(row)" | |
| :colspan="columns.length" | |
| class="bg-red-100" | |
| > | |
| {{ rowError(row) }} | |
| </td> | |
| <td | |
| v-for="column in columns" | |
| v-else | |
| :key="column.name" | |
| :class="[ | |
| 'group relative', | |
| colError(row, column) ? 'bg-red-100' : 'bg-blue-50', | |
| ]" | |
| > | |
| {{ column.field(row.value) }} | |
| <div | |
| v-if="colError(row, column)" | |
| :class="[ | |
| 'invisible group-hover:visible', | |
| 'opacity-100 group-hover:opacity-100', | |
| 'transition-opacity duration-300', | |
| 'absolute', | |
| 'bg-white', | |
| 'rounded text-xs text-gray-700', | |
| 'left-1/2 top-full mt-1 w-40 -translate-x-1/2 px-3 py-2 ', | |
| 'z-30', | |
| ]" | |
| > | |
| {{ colError(row, column) }} | |
| </div> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </template> | |
| </table> | |
| </div> | |
| </div> | |
| </template> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment