Я использую Pinia для управления состоянием различных таблиц данных в моем приложении. Несколько таблиц могут быть открыты «поверх» друг друга (т. е. в наложении друг на друга). Вот почему я переделываю нашу текущую настройку, в которой можно открыть только одну таблицу, а другая таблица перезаписывает предыдущую, в новую систему, в которой одновременно можно открыть несколько таблиц.
Я подумал об использовании Pinia с Map(), чтобы сохранить все конфигурации таблиц данных на месте, и использовать поле «activeDataTableName», чтобы отслеживать, какая таблица в данный момент является «активной».
Мой магазин Пинии выглядит так:
export const useDataTableStore = defineStore('data-table', {
state: () => ({
dataTables: new Map(), // use a Map to store all data table configurations
activeDataTableName: null, // Used to track which is the current active data table
}),
getters: {
getActiveDataTableConfig: (state) => {
const dtConfig = state.dataTables.get(state.activeDataTableName)
return dtConfig || null
},
getDataTableName: (state) => state.activeDataTableName,
getLoading: (state) => {
const dtConfig = state.dataTables.get(state.activeDataTableName)
return dtConfig ? dtConfig.loading : true
},
getDataTableColumns: (state) => {
const dtConfig = state.dataTables.get(state.activeDataTableName)
return dtConfig ? dtConfig.dataTableColumns : []
},
getTotalRecords: (state) => {
const dtConfig = state.dataTables.get(state.activeDataTableName)
return dtConfig ? dtConfig.totalRecords : 0
},
getFirst: (state) => {
const dtConfig = state.dataTables.get(state.activeDataTableName)
return dtConfig ? dtConfig.first : 0
},
getAmountRowsToShow: (state) => {
const dtConfig = state.dataTables.get(state.activeDataTableName)
return dtConfig ? dtConfig.amountRowsToShow : 15
},
getSelectedRows: (state) => {
const dtConfig = state.dataTables.get(state.activeDataTableName)
return dtConfig ? dtConfig.selectedRows : []
},
getFocussedRow: (state) => {
const dtConfig = state.dataTables.get(state.activeDataTableName)
return dtConfig ? dtConfig.focussedRow : null
},
getLazyParams: (state) => {
const dtConfig = state.dataTables.get(state.activeDataTableName)
return dtConfig ? dtConfig.lazyParams: {}
}
getSelectAll: (state) => {
const dtConfig = state.dataTables.get(state.activeDataTableName)
return dtConfig ? dtConfig.selectAll : false
},
},
actions: {
async initialize (nuxtApp, dtName) {
// The initialize function sets the dataTableName and loads the columns
const newDataTableConfigObject = {
loading: true,
dataTableName: dtName,
dataTableColumns: [],
totalRecords: 0,
first: 0,
amountRowsToShow: 15,
selectedRows: [],
focussedRow: null,
lazyParams: {},
selectAll: false,
}
this.dataTables.set(dtName, newDataTableConfigObject)
this.activeDataTableName = dtName
},
setSelectedRows (rows) {
const dtConfig = this.dataTables.get(this.activeDataTableName)
if (dtConfig) {
dtConfig.selectedRows = rows
}
},
},
})
Мои проблемы возникают, когда я пытаюсь выполнить двустороннюю привязку при использовании v-модели. Например: у меня будет компонент «DataTable», который принимает значения из моего магазина в v-модели следующим образом:
<DataTable
ref = "dataTableRef"
v-model:selection = "store.selectedRows"
v-model:first = "store.first"
v-model:rows = "store.amountRowsToShow"
...
/>
когда мой магазин инициализируется:
const store = useDataTableStore()
значения «selectedRows», «first» и т. д. будут находиться не непосредственно в хранилище, а в активном объекте конфигурации внутри dataTables в хранилище. Я также пытаюсь установить значения для переменных магазина:
store.amountRowsToShow = rows
store.lazyParams.first = store.getFirst
store.lazyParams.rows = store.getAmountRowsToShow
Вероятно, это можно решить с помощью специальных установщиков для каждого из них, но если мне придется создавать метод установки для каждого потенциального объекта внутри lazyParams, это становится очень утомительно. Нет ли лучшего подхода к этому?
ОБНОВЛЕНИЕ: я изменил магазин по рекомендации @Estus Flask. Сейчас это выглядит так:
import { defineStore, acceptHMRUpdate } from 'pinia'
import {
returnDataTableDefaultColumnsFullInfo,
returnDataTableColumnsFullInfoForFields,
returnDataTableAllColumns,
} from 'give-a-day-shared/data/dataTableConfig.mjs'
import { ResponseError } from 'give-a-day-shared/routes/responseUtils.mjs'
import { isSuperAuthor } from '../functions/frontendAuthUtils.js'
function createEmptyDataTableConfig (dtName) {
return {
showDataTable: false,
loading: true,
dataTableName: dtName,
allDataTableColumns: [],
dataTableColumns: [],
dataVisibilitySelectedOption: null,
dataVisibilityPlatformOptionVisible: null,
dataVisibilityUmbrellaOptionVisible: null,
totalRecords: 0,
first: 0,
amountRowsToShow: 15,
selectedRows: [],
focussedRow: null,
selectAll: false,
lazyParams: {}, // This variable is used to send to the backend
previewPanelVisible: true,
}
}
function setNestedProperty (obj, path, value) {
const keys = path.split('.')
let current = obj
for (let i = 0; i < keys.length - 1; i++) {
if (!current[keys[i]]) {
current[keys[i]] = {}
}
current = current[keys[i]]
}
current[keys[keys.length - 1]] = value
}
export const useDataTableStore = defineStore('data-table', {
state: () => ({
dataTables: new Map(), // use a Map to store all data table configurations
}),
getters: {
// BIG NOTE: Parameterized getters are NOT cached.
// This is why we create helpers that create computed's that use these getters to get the caching back.
getShowDataTable: (state) => (dtName) => state.dataTables.get(dtName)?.showDataTable || false,
getLoading: (state) => (dtName) => state.dataTables.get(dtName)?.loading || true,
getAllDataTableColumns: (state) => (dtName) => state.dataTables.get(dtName)?.allDataTableColumns || [],
getDataTableColumns: (state) => (dtName) => state.dataTables.get(dtName)?.dataTableColumns || [],
getDataVisibilitySelectedOption: (state) => (dtName) => state.dataTables.get(dtName)?.dataVisibilitySelectedOption || null,
getDataVisibilityPlatformOptionVisible: (state) => (dtName) => state.dataTables.get(dtName)?.dataVisibilityPlatformOptionVisible || null,
getDataVisibilityUmbrellaOptionVisible: (state) => (dtName) => state.dataTables.get(dtName)?.dataVisibilityUmbrellaOptionVisible || null,
getTotalRecords: (state) => (dtName) => state.dataTables.get(dtName)?.totalRecords || 0,
getFirst: (state) => (dtName) => state.dataTables.get(dtName)?.first || 0,
getAmountRowsToShow: (state) => (dtName) => state.dataTables.get(dtName)?.amountRowsToShow || 15,
getSelectedRows: (state) => (dtName) => state.dataTables.get(dtName)?.selectedRows || [],
getFocussedRow: (state) => (dtName) => state.dataTables.get(dtName)?.focussedRow || null,
getSelectAll: (state) => (dtName) => state.dataTables.get(dtName)?.selectAll || false,
getLazyParams: (state) => (dtName) => state.dataTables.get(dtName)?.lazyParams || {},
getPreviewPanelVisible: (state) => (dtName) => state.dataTables.get(dtName)?.previewPanelVisible || true,
},
actions: {
async initialize (nuxtApp, dtName) {
// The initialize function sets the dataTableConfig and loads the columns
this.resetConfig(dtName)
await this.fetchDataTableColumns(nuxtApp, dtName)
},
resetConfig (dtName) {
const newConfig = {
showDataTable: false,
loading: true,
dataTableName: dtName,
allDataTableColumns: [],
dataTableColumns: [],
dataVisibilitySelectedOption: null,
dataVisibilityPlatformOptionVisible: null,
dataVisibilityUmbrellaOptionVisible: null,
totalRecords: 0,
first: 0,
amountRowsToShow: 15,
selectedRows: [],
focussedRow: null,
selectAll: false,
lazyParams: {}, // This variable is used to send to the backend
previewPanelVisible: true,
}
this.dataTables.set(dtName, newConfig)
},
async fetchInitialDataToShowState (nuxtApp, dtName, hideDataToShowOptionPlatform = false, hideDataToShowOptionUmbrella = false) {
if (!isSuperAuthor(nuxtApp.$auth.user)) {
let dataTableConfig = this.dataTables.get(dtName)
if (!dataTableConfig) {
throw new ResponseError('no_data_table_config_found')
}
if (nuxtApp.$getPlatformOfUser() && !hideDataToShowOptionPlatform) {
let platform = await nuxtApp.$getFromBackend('/fetchPlatformById', { platformId: nuxtApp.$getPlatformOfUser().id })
if (platform) {
dataTableConfig.dataVisibilityPlatformOptionVisible = true
dataTableConfig.dataVisibilitySelectedOption = 'show_all'
}
}
if (nuxtApp.$globalState.toggledOrganisationId && !hideDataToShowOptionUmbrella) {
let umbrellaOrgIds = await nuxtApp.$getFromBackend('/fetchAllOrganisationIdsInTree', { organisationId: nuxtApp.$globalState.toggledOrganisationId, includeInvites: false })
if (umbrellaOrgIds.length > 1) {
dataTableConfig.dataVisibilityUmbrellaOptionVisible = true
if (!dataTableConfig.dataVisibilitySelectedOption) {
dataTableConfig.dataVisibilitySelectedOption = 'show_only_umbrella'
}
}
}
if (!dataTableConfig.dataVisibilitySelectedOption) {
dataTableConfig.dataVisibilitySelectedOption = 'show_only_myself'
}
}
},
async fetchDataTableColumns (nuxtApp, dtName) {
let dataTableConfig = this.dataTables.get(dtName)
if (!dataTableConfig) {
throw new ResponseError('no_data_table_config_found')
}
let allTableColumns = returnDataTableAllColumns(nuxtApp, dtName) // we use this for mapping the filter query params
if (allTableColumns) {
dataTableConfig.allDataTableColumns = allTableColumns
}
let existingTableOrgColumnsConfig = await nuxtApp.$getFromBackend('/fetchDataTableColumnConfiguration', { dataTableName: dataTableConfig.dataTableName })
if (existingTableOrgColumnsConfig) {
// Use the custom config of the organisation
let columnData = existingTableOrgColumnsConfig.columnData
if (typeof columnData === 'string') {
columnData = JSON.parse(columnData)
}
let columnsFullInfo = returnDataTableColumnsFullInfoForFields(nuxtApp, dataTableConfig.dataTableName, columnData)
dataTableConfig.dataTableColumns = columnsFullInfo
} else {
// Use the default config
let defaultColumnsFullInfo = returnDataTableDefaultColumnsFullInfo(nuxtApp, dataTableConfig.dataTableName)
dataTableConfig.dataTableColumns = defaultColumnsFullInfo
}
},
setLoading (dtName, bool) {
const dtConfig = this.dataTables.get(dtName)
if (dtConfig) {
dtConfig.loading = bool
}
},
setShowDataTable (dtName, bool) {
const dtConfig = this.dataTables.get(dtName)
if (dtConfig) {
dtConfig.showDataTable = bool
}
},
setSelectedRows (dtName, rows) {
const dtConfig = this.dataTables.get(dtName)
if (dtConfig) {
dtConfig.selectedRows = rows
}
},
setFirst (dtName, first) {
const dtConfig = this.dataTables.get(dtName)
if (dtConfig) {
dtConfig.first = first
}
},
setAmountRowsToShow (dtName, amountRowsToShow) {
const dtConfig = this.dataTables.get(dtName)
if (dtConfig) {
dtConfig.amountRowsToShow = amountRowsToShow
}
},
setTotalRecords (dtName, amount) {
const dtConfig = this.dataTables.get(dtName)
if (dtConfig) {
dtConfig.totalRecords = amount
}
},
setDataVisibilitySelectedOption (dtName, dataToShow) {
const dtConfig = this.dataTables.get(dtName)
if (dtConfig) {
dtConfig.dataVisibilitySelectedOption = dataToShow
}
},
setSelectAll (dtName, bool) {
const dtConfig = this.dataTables.get(dtName)
if (dtConfig) {
dtConfig.selectAll = bool
}
},
setFocussedRow (dtName, row) {
const dtConfig = this.dataTables.get(dtName)
if (dtConfig) {
dtConfig.focussedRow = row
}
},
setLazyParams (dtName, lazyParams) {
const dtConfig = this.dataTables.get(dtName)
if (dtConfig) {
dtConfig.lazyParams = lazyParams
}
},
setLazyParamsProperty (dtName, propertyPath, value) {
// The propertyPath is just a string value (with a . if it's a nested property) (eg: 'filters.organisationName')
const dtConfig = this.dataTables.get(dtName)
if (dtConfig) {
setNestedProperty(dtConfig.lazyParams, propertyPath, value)
}
},
},
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useDataTableStore, import.meta.hot))
}
И когда магазин Pinia загружается в мой компонент:
const dtStore = useDataTableStore()
Я получаю следующую ошибку:
[nuxt] [request error] [unhandled] [500] Converting circular structure to JSON
--> starting at object with constructor 'Object'
| property '_context' -> object with constructor 'Object'
--- property 'app' closes the circle
--> starting at object with constructor 'Object'
at JSON.stringify (<anonymous>)
Кажется, это происходит только при принудительном обновлении страницы (F5), а не при обычном переходе на страницу.
Наличие activeDataTableName
в хранилище — это нормально, хотя это состояние может принадлежать «активному» компоненту, а не хранилищу.
Существует фундаментальная проблема с геттерами типа getActiveDataTableConfig
. Если предполагается, что несколько таблиц будут отображаться одновременно, будет невозможно использовать геттеры для отображения данных в неактивных таблицах. Потенциально было бы меньше проблем, если бы все таблицы в хранилище рассматривались как равные. Для этого можно использовать параметризованные вычисления:
getters: {
getSelectedRows: (state) => {
return (name) => state.dataTables.get(name) || null;
},
...
Параметризованные вычисления не кэшируются, это может быть критично для ресурсоемкого использования, которым может быть таблица. Вместо или в дополнение к параметризованным вычислениям можно использовать помощник, который создает вычисления для конкретной таблицы:
function mapStoreTable(name) {
const store = useTableStore();
const tableData = store.dataTables.get(name);
const selectedRows = computed(() => store.getSelectedRows(name));
...
return { selectedRows, ... };
}
Это заставит selectedRows
и т. д. не реагировать на изменения в других таблицах.
Действие обновления может принимать имя таблицы для обновления определенной таблицы:
actions: {
setSelectedRows(name, rows) {
const dtConfig = this.dataTables.get(name);
if (dtConfig) {
dtConfig.selectedRows = rows
}
},
...
Мутировать магазин напрямую с помощью v-model
вообще сомнительная практика. Хотя это разрешено в Pinia, ранее в Vuex это не поощрялось, поскольку в результате получается код, который трудно отлаживать и поддерживать. В любом случае это было бы невозможно с предложенным помощником. v-model
можно обесцветить для реквизита и мероприятия. Или записываемый вычисляемый объект, который отправляет действие на набор и может использоваться как v-model:selection = "selectedRowsModel"
:
const { selectedRows } = mapStoreTable(tableName);
const selectedRowsModel = computed({
get() {
return selectedRows.value;
},
set(rows) {
store.setSelectedRows(tableName, rows)
}
});
И согласно вашему примеру во вспомогательной функции, для чего мне использовать переменную tableData?
Помощник переходит в отдельный .js-модуль. Он похож на помощника storeToRefs от Pinia. «tableData» — это состояние хранилища для одной таблицы. Вероятно, оно вам не понадобится, если вся логика хранилища хранится в геттерах и действиях. Используйте mapStoreTable в определенном компоненте в «настройке» для вывода определенной таблицы, когда имя уже известно. DataTable или его родительский элемент могут принять имя таблицы в качестве реквизита для связи компа с хранилищем. Никаких конкретных проблем, о которых я не мог подумать
Это так ясно! Большое спасибо!! Я уже отметил ответ как принятый. Однако я столкнулся с проблемой, из-за которой мой магазин Pinia выдает ошибку «Преобразование круговой структуры в JSON». Я изменил вопрос выше и добавил полностью переработанное определение магазина. Кажется, я не могу найти, в чем проблема. Это происходит только при нажатии CTRL+F5 для принудительного обновления страницы. При переходе на страницу без жесткого обновления кажется, что все работает нормально.
Похоже, это очень специфично для Nuxt и может быть сложно отладить. Похоже, он пытается сериализовать экземпляр приложения Vue в JSON на стороне сервера, что не имеет смысла. Магазин не имеет прямого отношения к проблеме. Ближайшая вещь, которая может косвенно вызвать это, - это горячая перезагрузка, попробуйте отключить import.meta.hot.accept... Также я бы предложил изменить процедуру инициализации, поскольку она, возможно, связана с жизненным циклом, проверьте, совпадает ли она, если вы это сделаете 'не вызываю инициализацию(), а заполняю ее поддельными данными. Также проверьте, происходит ли это в режиме prod.
Спасибо за ваш ответ! На данный момент это очень помогает. Мне было интересно, где мне определить вспомогательную функцию mapStoreTable(name)? Это внутри моего компонента с данными? А также в отношении сбора мусора, кэширования и прочего. Есть ли какие-либо предостережения, о которых мне следует знать при ваших предложениях?