Как создать динамически таблицу с диапазоном строк из ассоциативного массива с 5 глубинами VueJS

#javascript #vue.js #html-table

Вопрос:

Эта проблема кажется мне невыполнимой, но вот она

Итак, у меня есть ассоциативный массив, который можно изменить на основе введенных фильтров (например, столбцы могут исчезать или добавляться, если включены определенные фильтры). И у меня также есть таблица с динамическим «диапазоном строк» (поэтому таблица может быть очень длинной с правой стороны), которая создается на основе этого массива.

  {
  "countries": {
    "25": {
      "title": "France",
      "cities": {
        "8954": {
          "title": "Paris",
          "languages": {     <- here can add another object - "districts"
            "16": {
              "title": "English",
              "quarters": {
                "2_2020": {
                  "title": "% 2020-2021",
                  "parallels": {
                    "1": {
                      "title": "tst1"
                    },
                    "2": {
                      "title": "tst2",
                      "value": 7.89
                    },
                    "3": {
                      "title": "tst3",
                      "value": 37.2
                    },
                    "4": {
                      "title": "tst4",
                      "value": 9.16
                    },
                    "5": {
                      "title": "tst5",
                      "value": 6.45
                    }
                  }
                },
                "3_2020": {
                  "title": "% 2019-2020",
                  "parallels": {
                    "1": {
                      "title": "tst1"
                    },
                    "2": {
                      "title": "tst2",
                      "value": 8.59
                    },
                    "3": {
                      "title": "tst3",
                      "value": 9.1
                    },
                    "4": {
                      "title": "tst4",
                      "value": 6.8
                    },
                    "5": {
                      "title": "tst5",
                      "value": 75.1
                    }
                  }
                }
              }
            },
            "1000": {
              "title": "Spanish",
              "quarters": {
                "2_2020": {
                  "title": "% 2020-2021",
                  "parallels": {
                    "1": {
                      "title": "tst1"
                    },
                    "2": {
                      "title": "tst2",
                      "value": 2.75
                    },
                    "3": {
                      "title": "tst3",
                      "value": 41.2
                    },
                    "4": {
                      "title": "tst4",
                      "value": 6.97
                    },
                    "5": {
                      "title": "tst5",
                      "value": 74.4
                    }
                  }
                },
                "3_2020": {
                  "title": "% 2019-2020",
                  "parallels": {
                    "1": {
                      "title": "tst1"
                    },
                    "2": {
                      "title": "tst2",
                      "value": 8.51
                    },
                    "3": {
                      "title": "tst3",
                      "value": 99.1
                    },
                    "4": {
                      "title": "tst4",
                      "value": 75.8
                    },
                    "5": {
                      "title": "tst5",
                      "value": 25.11
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
 

и в идеале этот json выглядит так или (если существуют области объектов) так

Вот мой код vuejs для создания таблицы только для тела, используя другой объект — «район».

 <b-tr v-for="(itemCountry, indexCountry) in itemsReal.countries" :key="indexCountry">
  <b-tr v-for="(itemCities, indexCities) in itemCountry.cities" :key="indexCities" class="w-25 ">
    <b-th class="w-25">{{ itemCities.title }}</b-th>
      <b-tr v-for="(itemDistrict, indexDistricts) in itemCities.districts"  :key="indexDistricts">
        <b-td class="w-25 sticky-sidebar-district">{{ itemDistrict.title }}</b-td>
          <b-tr class="language-rows" v-for="(itemLanguages, indexLanguages) in itemDistrict.languages" :key="indexLanguages">
            <b-td class="language sticky-sidebar-language-District">{{ itemLanguages.title }}</b-td>
             <b-tr class="d-inline-flex" v-for="(itemQuarter, indexQuarter) in itemLanguages.quarters">
             <b-td v-for="(currentNumber, indexCurrentNumber) in itemQuarter.testsarr" :key="indexCurrentNumber" class="value-cells">
             {{ currentNumber.value }} {{ currentNumber.value === undefined ? 'amp;shy' : null }}
          </b-td>
        </b-tr>
      </b-tr>
    </b-tr>
  </b-tr>
</b-tr>
 

Ответ №1:

Это было определенно нелегко!

Прежде всего: vue-bootstrap работает не совсем так, как в вашем примере. Вы не можете просто гнездиться b-tr и b-td не использовать области видимости. Возможно, этого не хватает в вашем фрагменте, но это все равно не сработает, потому что

Второе: Вы не можете вложить tr s в td s и так далее. Вам нужно работать с rowspan и. colspan Таким образом, ваша таблица в обычном HTML (со стилями начальной загрузки) должна выглядеть следующим образом:

 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" integrity="sha384-KyZXEAg3QhqLMpG8r 8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">

<table class="table table-bordered">
  <tbody>
  <tr>
    <td rowspan="2">country</td>
    <td rowspan="2">cities</td>
    <td rowspan="2">languages</td>
    <td colspan="5">2019-2020</td>
    <td colspan="5">2020-2021</td>
  </tr>
  <tr>
    <td>tst1</td>
    <td>tst2</td>
    <td>tst3</td>
    <td>tst4</td>
    <td>tst5</td>
    <td>tst6</td>
    <td>tst7</td>
    <td>tst8</td>
    <td>tst9</td>
    <td>tst10</td>
  </tr>
  <tr>
   <th rowspan="4">France</th>
   <th rowspan="2">Paris</th>
   <td>english</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
  </tr>
  <tr>
   <td>spanish</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
  </tr>
  <tr>
   <th rowspan="2">Lyon</th>
   <td>english</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
  </tr>
  <tr>
   <td>spanish</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
   <td>empty</td>
  </tr>
 </tbody>
</table> 

Из-за того, как работают HTML-таблицы, не очень легко придумать циклы в vue, но мне удалось заставить это работать. Сначала мне нужно было немного отформатировать ваши данные, поэтому вместо объектов у меня были массивы. Таким образом, вам будет легче получить данные. Я написал для этого вычисляемую функцию на случай, если вы не имеете никакого влияния на форматирование входящих данных. Я добавил несколько комментариев в код. Я надеюсь, что это понятно.

 new Vue({
  el: "#app",
  data() {
    return {
      countries: {
        "25": {
          "title": "France",
          "cities": {
            "8954": {
              "title": "Paris",
              "languages": {
                "16": {
                  "title": "English",
                  "quarters": {
                    "2_2020": {
                      "title": "% 2020-2021",
                      "parallels": {
                        "1": {
                          "title": "tst1"
                        },
                        "2": {
                          "title": "tst2",
                          "value": 7.89
                        },
                        "3": {
                          "title": "tst3",
                          "value": 37.2
                        },
                        "4": {
                          "title": "tst4",
                          "value": 9.16
                        },
                        "5": {
                          "title": "tst5",
                          "value": 6.45
                        }
                      }
                    },
                    "3_2020": {
                      "title": "% 2019-2020",
                      "parallels": {
                        "1": {
                          "title": "tst1"
                        },
                        "2": {
                          "title": "tst2",
                          "value": 8.59
                        },
                        "3": {
                          "title": "tst3",
                          "value": 9.1
                        },
                        "4": {
                          "title": "tst4",
                          "value": 6.8
                        },
                        "5": {
                          "title": "tst5",
                          "value": 75.1
                        }
                      }
                    }
                  }
                },
                "1000": {
                  "title": "Spanish",
                  "quarters": {
                    "2_2020": {
                      "title": "% 2020-2021",
                      "parallels": {
                        "1": {
                          "title": "tst1"
                        },
                        "2": {
                          "title": "tst2",
                          "value": 2.75
                        },
                        "3": {
                          "title": "tst3",
                          "value": 41.2
                        },
                        "4": {
                          "title": "tst4",
                          "value": 6.97
                        },
                        "5": {
                          "title": "tst5",
                          "value": 74.4
                        }
                      }
                    },
                    "3_2020": {
                      "title": "% 2019-2020",
                      "parallels": {
                        "1": {
                          "title": "tst1"
                        },
                        "2": {
                          "title": "tst2",
                          "value": 8.51
                        },
                        "3": {
                          "title": "tst3",
                          "value": 99.1
                        },
                        "4": {
                          "title": "tst4",
                          "value": 75.8
                        },
                        "5": {
                          "title": "tst5",
                          "value": 25.11
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  computed: {
    formattedCountries() {
      return Object.keys(this.countries).map(countryKey => {
        const country = this.countries[countryKey]

        return {
          id: countryKey,
          title: country.title,
          cities: Object.keys(country.cities).map(cityKey => {
            const city = country.cities[cityKey]

            return {
              id: cityKey,
              title: city.title,
              languages: Object.keys(city.languages).map(languageKey => {
                const language = city.languages[languageKey]

                return {
                  id: languageKey,
                  title: language.title,
                  values: Object.keys(language.quarters).map(quarterKey => {
                    const quarter = language.quarters[quarterKey]

                    return Object.keys(quarter.parallels).map(parallelKey => {
                      const parallel = quarter.parallels[parallelKey]

                      return parallel.value ? parallel.value : null 
                    })
                  }).reduce((a, b) => a.concat(b), [])
                }
              })
            }
          })
        }
      })
    }
  },
  methods: {
    calculateCountryRowspan(country) {
      return country.cities.reduce((a, b) => {
        return a   b
      }, 0)
    }
  }
}) 
 <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>

<div id="app">
  <table class="table table-bordered">
    <thead>
      <tr>
        <th rowspan="2">country</th>
        <th rowspan="2">cities</th>
        <th rowspan="2">languages</th>
        <th colspan="5">2019-2020</th>
        <th colspan="5">2020-2021</th>
      </tr>
      <tr>
        <th>tst1</th>
        <th>tst2</th>
        <th>tst3</th>
        <th>tst4</th>
        <th>tst5</th>
        <th>tst6</th>
        <th>tst7</th>
        <th>tst8</th>
        <th>tst9</th>
        <th>tst10</th>
      </tr>
    </thead>

    <!-- Loop over coutries -->
    <tbody v-for="country in formattedCountries" :key="'country'   country.id">
      <tr>
        <!-- The rowspan of the countries title needs to be the count of its cities times its languages -->
        <th :rowspan="calculateCountryRowspan(country)">
          {{ country.title }}
        </th>

        <!-- The countries title is followed its first city and the cities values -->
        <template v-if="country.cities.length">
          <!-- The rowspan of the city title needs to be the count of its languages -->
          <th :rowspan="country.cities[0].languages.length">
            {{ country.cities[0].title }}
          </th>

          <th>
            {{ country.cities[0].languages[0].title }}
          </th>

          <td v-for="(value, valueIndex) in country.cities[0].languages[0].values" :key="'value'   valueIndex">
            {{ value }}
          </td>
        </template>
      </tr>

      <template v-if="country.cities.length">
        <!-- If the city has more than one language, add the others as new rows
          (languageIndex starts with 1! The loop skips the first language from above) -->
        <tr v-for="languageIndex in country.cities[0].languages.length - 1" :key="'city0language'   languageIndex">
          <th>
            {{ country.cities[0].languages[languageIndex].title }}
          </th>

          <td v-for="(value, valueIndex) in country.cities[0].languages[languageIndex].values" :key="'value'   valueIndex">
            {{ value }}
          </td>
        </tr>
      </template>

      <!-- If there are more than one city, add them linke before
          (cityIndex starts with 1! The loop skips the first city from above) -->
      <template v-for="cityIndex in country.cities.length - 1">
        <tr :key="'city'   cityIndex   'row1'">
          <th :rowspan="country.cities[cityIndex].languages.length">
            {{ country.cities[cityIndex].title }}
          </th>
          <th>
            {{ country.cities[cityIndex].languages[0].title }}
          </th>

          <td v-for="(value, valueIndex) in country.cities[cityIndex].languages[0].values" :key="'value'   valueIndex">
            {{ value }}
          </td>
        </tr>
        
        <tr v-for="languageIndex in country.cities[cityIndex].languages.length - 1" :key="'city'   cityIndex   'language'   languageIndex">
          <th>
            {{ country.cities[cityIndex].languages[languageIndex].title }}
          </th>

          <td v-for="(value, valueIndex) in country.cities[cityIndex].languages[languageIndex].values" :key="'value'   valueIndex">
            {{ value }}
          </td>
        </tr>
      </template>
    </tbody>
  </table>
</div> 

Комментарии:

1. Этот пример был идеальным для меня, но при добавлении другого столбца все ломается. Я попытался добавить <template> и сделать еще один цикл внутри него, но все не так. Не могли бы вы еще немного помочь с этим json , пожалуйста?

2. Может быть, сначала попробуйте решить ее с помощью статического HTML. Это могло бы помочь лучше понять структуру. Я также нашел, что это очень сложно, чтобы сделать все правильно.

3. Ах, хорошо, я все сделал правильно, спасибо вам большое за ваш пример. Теперь я сяду и напишу дополнительный ответ здесь