DefaultTableServerSide.vue 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. # DefaultTableServerSide.vue
  2. <template>
  3. <q-table
  4. v-model:fullscreen="fullscreen"
  5. v-model:pagination="pagination"
  6. row-key="id"
  7. flat
  8. class="softpar-table q-pa-sm"
  9. :pagination-label="getPaginationLabel"
  10. :rows="rows"
  11. :rows-per-page-label="$t('common.ui.table.rows_per_page')"
  12. :columns="columns"
  13. :visible-columns="visibleColumns"
  14. :filter="pagination.filter"
  15. :grid="$q.screen.lt.sm"
  16. :loading="loading"
  17. :hide-top="semTableTop"
  18. v-bind="$attrs"
  19. @row-click="onRowClick"
  20. >
  21. <template v-if="!semTableTop" #top>
  22. <div
  23. class="flex full-width justify-between items-center"
  24. >
  25. <DefaultInput
  26. v-if="showSearchField"
  27. v-model="pagination.filter"
  28. outlined
  29. dense
  30. debounce="500"
  31. :placeholder="$t('associado.search_placeholder')"
  32. clearable
  33. autofocus
  34. color="violet-normal"
  35. >
  36. <template #prepend>
  37. <q-icon name="mdi-magnify" />
  38. </template>
  39. </DefaultInput>
  40. <DefaultSelect
  41. v-if="showColumnsSelect"
  42. v-model="visibleColumns"
  43. class="q-ml-md"
  44. multiple
  45. dense
  46. outlined
  47. options-outlined
  48. :display-value="$q.lang.table.columns"
  49. emit-value
  50. map-options
  51. :options="mapColumns"
  52. style="min-width: 150px"
  53. options-selected-class="text-bold"
  54. />
  55. <q-space v-if="showSearchField || showColumnsSelect" />
  56. <q-btn
  57. v-if="addItem"
  58. color="primary"
  59. padding="12px 16px"
  60. :outline="outlineAdd"
  61. :label="labelAdd"
  62. @click="onAddItem"
  63. />
  64. </div>
  65. </template>
  66. <template #body-cell-actions="{ row }">
  67. <q-td auto-width>
  68. <q-item-section class="no-wrap" style="flex-direction: row">
  69. <slot name="body-cell-actions" :row="row" />
  70. <q-btn
  71. v-if="deleteFunction"
  72. color="negative"
  73. flat
  74. dense
  75. icon="mdi-delete"
  76. style="width: 45px"
  77. class="q-ml-auto q-mr-sm"
  78. @click.prevent.stop="onDelete(row.id)"
  79. />
  80. </q-item-section>
  81. </q-td>
  82. </template>
  83. <template v-if="!hideNoDataLabel" #no-data>
  84. <div class="q-my-md row justify-center full-width">
  85. <q-spinner v-if="loading" color="primary" size="30px" />
  86. <div v-else class="q-pa-md body2">
  87. {{ $t("http.errors.no_records_found") }}
  88. </div>
  89. </div>
  90. </template>
  91. <template #bottom="scope">
  92. <div class="flex full-width justify-end items-center" style="gap: 8px">
  93. <span class="text-caption text-grey-7">
  94. {{ $t("common.ui.table.rows_per_page") }} 10 &nbsp;|&nbsp;
  95. {{ pagination.from }}-{{ pagination.to }}
  96. {{ $t("common.ui.table.of") }}
  97. {{ pagination.rowsNumber }}
  98. </span>
  99. <q-btn
  100. icon="mdi-chevron-left"
  101. color="grey-8"
  102. round
  103. dense
  104. flat
  105. :disable="scope.isFirstPage"
  106. @click="prevPage"
  107. />
  108. <q-btn
  109. icon="mdi-chevron-right"
  110. color="grey-8"
  111. round
  112. dense
  113. flat
  114. :disable="scope.isLastPage"
  115. @click="nextPage"
  116. />
  117. </div>
  118. </template>
  119. <template v-for="slotName in usableSlots($slots)" #[slotName]="data">
  120. <slot :name="slotName" v-bind="data" />
  121. </template>
  122. </q-table>
  123. </template>
  124. <script setup>
  125. import { ref, computed, onMounted, toRaw, watch } from "vue";
  126. import { useI18n } from "vue-i18n";
  127. import { useRouter } from "vue-router";
  128. import DefaultInput from "./DefaultInput.vue";
  129. import DefaultSelect from "./DefaultSelect.vue";
  130. const emit = defineEmits([
  131. "onRowClick",
  132. "onAddItem",
  133. "noRows",
  134. "togglePrincipal",
  135. ]);
  136. const {
  137. columns,
  138. apiCall,
  139. outlineAdd,
  140. openItem,
  141. openItemRoute,
  142. addItem,
  143. addItemRoute,
  144. rowsPerPage,
  145. showSearchField,
  146. hideNoDataLabel,
  147. deleteFunction,
  148. semTableTop,
  149. } = defineProps({
  150. columns: {
  151. type: Array,
  152. required: true,
  153. },
  154. apiCall: {
  155. type: Function,
  156. required: true,
  157. },
  158. labelAdd: {
  159. type: String,
  160. default: "Adicionar",
  161. },
  162. outlineAdd: {
  163. type: Boolean,
  164. default: false,
  165. },
  166. openItem: {
  167. type: Boolean,
  168. default: false,
  169. },
  170. openItemRoute: {
  171. type: String,
  172. default: "",
  173. },
  174. addItem: {
  175. type: Boolean,
  176. default: true,
  177. },
  178. addItemRoute: {
  179. type: String,
  180. default: "",
  181. },
  182. rowsPerPage: {
  183. type: Number,
  184. default: 10,
  185. },
  186. showColumnsSelect: {
  187. type: Boolean,
  188. default: false,
  189. },
  190. showSearchField: {
  191. type: Boolean,
  192. default: true,
  193. },
  194. noApiRoute: {
  195. type: Boolean,
  196. default: false,
  197. },
  198. hideNoDataLabel: {
  199. type: Boolean,
  200. default: false,
  201. },
  202. deleteFunction: {
  203. type: Function,
  204. default: null,
  205. },
  206. semTableTop: {
  207. type: Boolean,
  208. default: false,
  209. },
  210. });
  211. const { t } = useI18n();
  212. const router = useRouter();
  213. const rows = ref([]);
  214. const loading = ref(true);
  215. const fullscreen = ref(false);
  216. const pagination = ref({
  217. filter: undefined,
  218. page: 1,
  219. rowsPerPage: rowsPerPage,
  220. rowsNumber: 0,
  221. from: 0,
  222. to: 0,
  223. });
  224. const mapColumns = computed(() => {
  225. return columns.reduce((accm, column) => {
  226. if (!column.required) {
  227. accm.push({
  228. label: column.label.toUpperCase(),
  229. value: column.name,
  230. });
  231. }
  232. return accm;
  233. }, []);
  234. });
  235. const visibleColumns = ref(mapColumns.value.map((column) => column.value));
  236. const onRowClick = (evt, row, index) => {
  237. const item = toRaw(row);
  238. if (openItem) {
  239. if (openItemRoute) {
  240. router.push({ name: openItemRoute, params: { id: item.id } });
  241. } else {
  242. emit("onRowClick", { evt, row, index });
  243. }
  244. }
  245. };
  246. const onAddItem = () => {
  247. if (addItem) {
  248. if (addItemRoute) {
  249. router.push({ name: addItemRoute });
  250. } else {
  251. emit("onAddItem");
  252. }
  253. }
  254. };
  255. const onDelete = async (id) => {
  256. if (deleteFunction) {
  257. loading.value = true;
  258. try {
  259. await deleteFunction(id);
  260. await onRequest();
  261. } catch (error) {
  262. console.error(error);
  263. } finally {
  264. loading.value = false;
  265. }
  266. }
  267. };
  268. const prevPage = () => {
  269. pagination.value.page--;
  270. onRequest();
  271. };
  272. const nextPage = () => {
  273. pagination.value.page++;
  274. onRequest();
  275. };
  276. const getPaginationLabel = (from, to, total) => {
  277. return `${from}-${to} ${t?.("common.ui.table.of") ?? "of"} ${total}`;
  278. };
  279. let isFetching = false;
  280. const onRequest = async () => {
  281. if (isFetching) return;
  282. isFetching = true;
  283. loading.value = true;
  284. try {
  285. const response = await apiCall({
  286. page: pagination.value.page,
  287. perPage: pagination.value.rowsPerPage,
  288. filter: pagination.value.filter,
  289. });
  290. rows.value = response.data.result.data;
  291. pagination.value.rowsNumber = response.data.result.total;
  292. pagination.value.from = response.data.result.from;
  293. pagination.value.to = response.data.result.to;
  294. if (rows.value.length === 0) {
  295. emit("noRows");
  296. }
  297. } catch (error) {
  298. console.error("Error fetching data:", error);
  299. } finally {
  300. loading.value = false;
  301. isFetching = false;
  302. }
  303. };
  304. watch(
  305. () => apiCall,
  306. () => onRequest(),
  307. );
  308. watch(
  309. () => pagination.value.filter,
  310. (newFilter, oldFilter) => {
  311. if (newFilter === oldFilter || loading.value) return;
  312. pagination.value.page = 1;
  313. onRequest();
  314. },
  315. );
  316. watch(
  317. () => pagination.value.rowsPerPage,
  318. (newVal, oldVal) => {
  319. if (newVal === oldVal || loading.value) return;
  320. pagination.value.page = 1;
  321. onRequest();
  322. },
  323. );
  324. onMounted(async () => {
  325. await onRequest();
  326. });
  327. const usableSlots = (slots) => {
  328. const handled = ["top", "body-cell-actions", "no-data", "bottom", "loading"];
  329. return Object.keys(slots).filter((s) => !handled.includes(s));
  330. };
  331. defineExpose({
  332. refresh: onRequest,
  333. });
  334. </script>
  335. <style lang="scss">
  336. @import "src/css/table.scss";
  337. </style>