DefaultTable.vue 7.9 KB

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