expandだけだとテーブル構造としては1行しか表示できず、hoverもしない。
追記:item全体をslotすると2行以上表示できて、hoverも出来ました。
一覧で表示し切れない情報を補足的に表示するには良さそう。
今回は2行以上をテーブル構造で表示して、hoverもしたかったので、
search/custom-filterとの組み合わせる事にしました。

expandを試す

item全体をslotしていない場合

複数行出すには、それぞれのtdの中で、並べるしかなさそう。
高さが変わらないように改行制御が必要。hoverも自前で設定する必要がある。

<template>
  <v-data-table
    :headers="headers"
    :items="items"
    item-key="id"
    :items-per-page="-1"
    hide-default-footer
    mobile-breakpoint="0"
    fixed-header
    :height="Math.max(200, $vuetify.breakpoint.height - 146) + 'px'"
    disable-sort
    show-expand
  >
    <!-- 展開 -->
    <template #expanded-item="{ item }">
      <!-- slot外でtrで囲まれているので、trで複数行出せない。hoverもしない -->
      <td class="px-1" />
      <td class="pl-5 pr-1">
        {{ item.name }}の内訳
      </td>
      <td class="text-end pl-1 pr-6">
        500
      </td>
      <td class="text-end pl-1 pr-6">
        700
      </td>
    </template>
    <!-- 値1 -->
    <template #[`item.value1`]="{ item }">
      {{ item.value1.toLocaleString() }}
    </template>
    <!-- 値2 -->
    <template #[`item.value2`]="{ item }">
      {{ item.value2.toLocaleString() }}
    </template>
  </v-data-table>
</template>

<script>
export default {
  data () {
    return {
      headers: [
        { value: 'data-table-expand', align: 'end', class: 'px-1', cellClass: 'px-1' },
        { text: '名称', value: 'name', cellClass: 'font-weight-bold px-1', width: '75%' },
        { text: '値1', value: 'value1', align: 'end', class: 'pr-6', cellClass: 'pl-1 pr-6', width: 100 },
        { text: '値2', value: 'value2', align: 'end', class: 'pr-6', cellClass: 'pl-1 pr-6', width: 100 }
      ],
      items: [
        { id: 10, name: '項目1', value1: 1500, value2: 2100 },
        { id: 20, name: '項目2', value1: 1700, value2: 2300 },
        { id: 30, name: '項目3', value1: 1900, value2: 2500 }
      ]
    }
  }
}
</script>

追記:item全体をslotした場合

slot外でtrで囲まれなくなるので、trで複数行出せ。hoverする事も確認出来ました。
共通化や操作性の向上のコードを一部変更して確認済み。

    <template #expanded-item="{ item }">
      <tr>
        <td>{{ item.name }}の内訳1</td>
      </tr>
      <tr>
        <td>{{ item.name }}の内訳2</td>
      </tr>
    </template>

search/custom-filterとの組み合わせる

itemsに内訳も入れておいて、expandをトリガーにcustom-filterで絞り込んで表示しています。
hoverもされます。

expandの表示制御や、classを入れて背景色の変更も試してみました。
挙動等は、ソースのコメントを参照してください。

<template>
  <v-data-table
    :headers="headers"
    :items="items"
    item-key="id"
    :items-per-page="-1"
    hide-default-footer
    mobile-breakpoint="0"
    fixed-header
    :height="Math.max(200, $vuetify.breakpoint.height - 146) + 'px'"
    disable-sort
    :item-class="itemClass"
    :search="searchCount.toString()"
    :custom-filter="customFilter"
    show-expand
    :expanded.sync="expanded"
    @item-expanded="itemExpanded"
  >
    <!-- 行番号 // データに持たせなくても連番を表示できる。但し、展開すると番号が振り直される -->
    <template #[`item.index`]="{ index }">
      {{ (index + 1).toLocaleString() }}
    </template>
    <!-- 展開 // v-data-table__expand-icon--activeを入れたり消したりするとアイコンが回転して向きが変わる -->
    <template #[`item.data-table-expand`]="{ item, expand, isExpanded }">
      <v-icon
        v-if="item.expand"
        :class="isExpanded ? 'v-data-table__expand-icon--active' : ''"
        @click="expand(!isExpanded)"
      >
        mdi-chevron-down
      </v-icon>
    </template>
    <!-- 名称 -->
    <template #[`item.name`]="{ item }">
      <span :class="item.parent_id == null ? 'font-weight-bold' : 'pl-4'">
        {{ item.name }}
      </span>
    </template>
    <!-- 値1 -->
    <template #[`item.value1`]="{ item }">
      <span :class="item.parent_id == null ? valueClass(item.value1) : ''">
        {{ item.value1.toLocaleString() }}
      </span>
    </template>
    <!-- 値2 -->
    <template #[`item.value2`]="{ item }">
      <span :class="item.parent_id == null ? valueClass(item.value2) : ''">
        {{ item.value2.toLocaleString() }}
      </span>
    </template>
  </v-data-table>
</template>

<script>
export default {
  data () {
    return {
      searchCount: 0, // searchに値を入れると初回表示前にcustom-filterが呼び出される
      expanded: [], // 展開した行のitemが追加、閉じたら対象のitemが削除される
      headers: [
        { value: 'index', align: 'end', class: 'px-2', cellClass: 'col_index px-2' },
        { value: 'data-table-expand', class: 'px-1', cellClass: 'px-1' },
        { text: '名称', value: 'name', cellClass: 'px-1', width: '75%' },
        { text: '値1', value: 'value1', align: 'end', class: 'pr-6', cellClass: 'pl-1 pr-6', width: 100 },
        { text: '値2', value: 'value2', align: 'end', class: 'pr-6', cellClass: 'pl-1 pr-6', width: 100 }
      ],
      items: [
        // expand: 展開アイコンを表示するか否か, parent_id: 親のID(無い場合は親と判定)
        { id: 10, expand: true, name: '項目1', value1: 1500, value2: 2100 },
        { id: 11, parent_id: 10, name: '項目1の内訳1', value1: 500, value2: 700 },
        { id: 12, parent_id: 10, name: '項目1の内訳2', value1: 1000, value2: 1400 },
        { id: 20, expand: true, name: '項目2', value1: 1700, value2: 2300 },
        { id: 21, parent_id: 20, name: '項目2の内訳1', value1: 600, value2: 800 },
        { id: 22, parent_id: 20, name: '項目2の内訳2', value1: 1100, value2: 1500 },
        { id: 30, expand: false, name: '項目3', value1: 1900, value2: 2500 }
      ]
    }
  },

  methods: {
    // 行毎に任意のclassを入れられる。CSSでデザイン変えるのに使える
    itemClass (item) {
      // eslint-disable-next-line no-console
      console.log('itemClass', item)

      return item.parent_id == null ? 'row_parent' : 'row_child'
    },

    // searchの値が変わると行毎に呼び出される。trueを返した行が表示される
    customFilter (value, search, item) {
      // eslint-disable-next-line no-console
      console.log('customFilter', value, search, item)

      if (item.parent_id == null) {
        return true
      } else {
        return this.expanded.some(expand => expand.id === item.parent_id)
      }
    },

    // 展開したり閉じたりすると呼び出される
    itemExpanded ({ item, value }) {
      // eslint-disable-next-line no-console
      console.log('itemExpanded', item, value)

      this.searchCount += 1 // custom-filterが呼び出されるように値を変更
    },

    valueClass (value) {
      if (value <= 1700) {
        return 'red--text'
      } else if (value <= 2100) {
        return 'yellow--text'
      } else {
        return 'blue--text'
      }
    }
  }
}
</script>

<style scoped>
/* ヘッダの背景色を変える */
.v-data-table tr > th {
  background: dimgray !important;
}
/* 親項目の背景色を変える */
.v-data-table .row_parent {
  background-color: gray;
}
/* 子項目の背景色を変える */
.v-data-table .row_child {
  background-color: darkgray;
}
/* 子項目の行番号は親と同じ背景色に変える */
.v-data-table .row_child > .col_index {
  background-color: gray;
}
/* hoverの色を変える */
.v-data-table tr:hover > td {
  background-color: black !important;
}
</style>

共通化や操作性の向上

slotの値1と値2は同じ処理なので共通化したい。
→ 要素毎だと出来ないので、slotをitem全体に変更

slotの中でtr/tdを定義する事になるので、item-classやheadersのcellClassは効かなくなる。
→ 削除して、tr/tdに直接定義に変更

名称のエリアをクリックしても展開や閉じるられるようにしました。
→ 要素毎のslotだと引数にexpandがないので、簡単に出来なかったけど、全体なら簡単

<template>
  <v-data-table
    :headers="headers"
    :items="items"
    item-key="id"
    :items-per-page="-1"
    hide-default-footer
    mobile-breakpoint="0"
    fixed-header
    :height="Math.max(200, $vuetify.breakpoint.height - 146) + 'px'"
    disable-sort
    :search="searchCount.toString()"
    :custom-filter="customFilter"
    show-expand
    :expanded.sync="expanded"
    @item-expanded="itemExpanded"
  >
    <template #item="{ index, item, expand, isExpanded }">
      <tr :class="item.parent_id == null ? 'row_parent' : 'row_child'">
        <!-- 行番号 // データに持たせなくても連番を表示できる。但し、展開すると番号が振り直される -->
        <td class="text-end col_index px-2">
          {{ (index + 1).toLocaleString() }}
        </td>
        <!-- 展開 // v-data-table__expand-icon--activeを入れたり消したりするとアイコンが回転して向きが変わる -->
        <td class="px-1">
          <v-icon
            v-if="item.expand"
            :class="isExpanded ? 'v-data-table__expand-icon--active' : ''"
            @click="expand(!isExpanded)"
          >
            mdi-chevron-down
          </v-icon>
        </td>
        <!-- 名称 // エリアをクリックで展開 or 閉じる -->
        <td class="px-1" @click="item.expand ? expand(!isExpanded) : void(0)">
          <span :class="item.parent_id == null ? 'font-weight-bold' : 'pl-4'">
            {{ item.name }}
          </span>
        </td>
        <!-- 値1・2 -->
        <td v-for="value of [item.value1, item.value2]" :key="value" class="text-end pl-1 pr-6">
          <span :class="item.parent_id == null ? valueClass(value) : ''">
            {{ value.toLocaleString() }}
          </span>
        </td>
      </tr>
    </template>
    <!-- これがないと展開した時にエラーになる。TypeError: this.$scopedSlots.expanded-item is not a function -->
    <template #expanded-item />
  </v-data-table>
</template>

<script>
export default {
  data () {
    return {
      searchCount: 0, // searchに値を入れると初回表示前にcustom-filterが呼び出される
      expanded: [], // 展開した行のitemが追加、閉じたら対象のitemが削除される
      headers: [
        { value: 'index', class: 'px-2' },
        { value: 'data-table-expand', class: 'px-1' },
        { text: '名称', value: 'name', width: '75%' },
        { text: '値1', value: 'value1', align: 'end', class: 'pr-6', width: 100 },
        { text: '値2', value: 'value2', align: 'end', class: 'pr-6', width: 100 }
      ],
      items: [
        // expand: 展開アイコンを表示するか否か, parent_id: 親のID(無い場合は親と判定)
        { id: 10, expand: true, name: '項目1', value1: 1500, value2: 2100 },
        { id: 11, parent_id: 10, name: '項目1の内訳1', value1: 500, value2: 700 },
        { id: 12, parent_id: 10, name: '項目1の内訳2', value1: 1000, value2: 1400 },
        { id: 20, expand: true, name: '項目2', value1: 1700, value2: 2300 },
        { id: 21, parent_id: 20, name: '項目2の内訳1', value1: 600, value2: 800 },
        { id: 22, parent_id: 20, name: '項目2の内訳2', value1: 1100, value2: 1500 },
        { id: 30, expand: false, name: '項目3', value1: 1900, value2: 2500 }
      ]
    }
  },

  methods: {
    // searchの値が変わると行毎に呼び出される。trueを返した行が表示される
    customFilter (value, search, item) {
      // eslint-disable-next-line no-console
      console.log('customFilter', value, search, item)

      if (item.parent_id == null) {
        return true
      } else {
        return this.expanded.some(expand => expand.id === item.parent_id)
      }
    },

    // 展開したり閉じたりすると呼び出される
    itemExpanded ({ item, value }) {
      // eslint-disable-next-line no-console
      console.log('itemExpanded', item, value)

      this.searchCount += 1 // custom-filterが呼び出されるように値を変更
    },

    valueClass (value) {
      if (value <= 1700) {
        return 'red--text'
      } else if (value <= 2100) {
        return 'yellow--text'
      } else {
        return 'blue--text'
      }
    }
  }
}
</script>

<style scoped>
/* ヘッダの背景色を変える */
.v-data-table tr > th {
  background: dimgray !important;
}
/* 親項目の背景色を変える */
.v-data-table .row_parent {
  background-color: gray;
}
/* 子項目の背景色を変える */
.v-data-table .row_child {
  background-color: darkgray;
}
/* 子項目の行番号は親と同じ背景色に変える */
.v-data-table .row_child > .col_index {
  background-color: gray;
}
/* hoverの色を変える */
.v-data-table tr:hover > td {
  background-color: black !important;
}
</style>

あとは、APIでitemsを一括で取得するか、
初回は親項目だけ取得して、expandをトリガーに子項目を取得して、親項目の下に挿入すれば完成。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です