DefaultTableServerSide.vue 8.2 KB

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