Initial commit

This commit is contained in:
2025-08-04 16:33:07 +03:30
commit f798e8e35c
9595 changed files with 1208683 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
<script setup>
import data from '@/views/demos/forms/tables/data-table/datatable'
const headers = [
{
title: 'ID',
sortable: false,
key: 'id',
},
{
title: 'NAME',
key: 'fullName',
},
{
title: 'EMAIL',
key: 'email',
},
{
title: 'DATE',
key: 'startDate',
},
{
title: 'EXPERIENCE',
key: 'experience',
},
{
title: 'AGE',
key: 'age',
},
]
</script>
<template>
<VDataTable
:headers="headers"
:items="data"
:items-per-page="5"
/>
</template>

View File

@@ -0,0 +1,211 @@
<script setup>
import { ref, watch, onMounted, onUnmounted } from 'vue'
import { AgGridVue } from 'ag-grid-vue3'
import data from '@/views/demos/forms/tables/data-table/datatable'
import { ModuleRegistry, AllCommunityModule } from 'ag-grid-community'
import { ExcelExportModule } from 'ag-grid-enterprise'
import DataTableHeader from './components/DataTableHeader.vue'
import UserDetailsDialog from './components/UserDetailsDialog.vue'
import ConfirmDeleteDialog from './components/ConfirmDeleteDialog.vue'
import { useDataTableGrid } from './composables/useDataTableGrid'
import { useDataTableActions } from './composables/useDataTableActions'
import { useResponsive } from './composables/useResponsive'
import jsPDF from 'jspdf'
import autoTable from 'jspdf-autotable'
ModuleRegistry.registerModules([AllCommunityModule, ExcelExportModule])
const { isMobile, isTablet, windowWidth } = useResponsive()
const {
gridApi,
columnDefs,
rowData,
defaultColDef,
gridOptions,
onGridReady: gridReady,
onCellValueChanged,
updateColumnVisibility,
updatePagination
} = useDataTableGrid(data, isMobile, isTablet)
const {
setupGlobalHandlers,
cleanupGlobalHandlers,
exportToCSV,
exportToExcel,
saveUser,
confirmDelete,
cancelDelete,
showDetailsDialog,
selectedRowData,
deleteRowData,
showDeleteDialog,
deleteRow
} = useDataTableActions(gridApi, rowData)
const quickFilter = ref('')
watch(quickFilter, (newValue) => {
if (gridApi.value) {
gridApi.value.setGridOption('quickFilterText', newValue || '')
}
}, { immediate: false })
const onGridReady = params => {
gridReady(params)
setupGlobalHandlers()
}
const handleSaveUser = updatedData => saveUser(updatedData)
const handleQuickFilterUpdate = (value) => {
quickFilter.value = value || ''
if (gridApi.value) {
gridApi.value.setGridOption('quickFilterText', value || '')
}
}
onMounted(() => {
setupGlobalHandlers()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
cleanupGlobalHandlers()
window.removeEventListener('resize', handleResize)
})
const handleResize = () => {
windowWidth.value = window.innerWidth
if (gridApi.value) {
updateColumnVisibility()
updatePagination()
setTimeout(() => {
if (gridApi.value && gridApi.value.sizeColumnsToFit) {
gridApi.value.sizeColumnsToFit()
}
}, 100)
}
}
const exportToPDF = () => {
if (!gridApi.value) {
console.error('Grid API not available')
return
}
try {
const doc = new jsPDF()
const rowDataForPDF = []
const headers = []
columnDefs.value.forEach(col => {
if (col.headerName && col.field && col.field !== 'action') {
headers.push(col.headerName)
}
})
gridApi.value.forEachNodeAfterFilterAndSort(node => {
const row = []
columnDefs.value.forEach(col => {
if (col.field && col.field !== 'action') {
let value = node.data[col.field] || ''
if (col.field === 'salary' && value) {
value = `$${Math.floor(Number(value)).toLocaleString()}`
} else if (col.field === 'startDate' && value) {
const date = new Date(value)
if (!isNaN(date.getTime())) {
const day = date.getDate().toString().padStart(2, '0')
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const year = date.getFullYear()
value = `${day}/${month}/${year}`
}
} else if (col.field === 'status') {
const statusMap = {
0: 'Applied',
1: 'Current',
2: 'Professional',
3: 'Rejected',
4: 'Resigned'
}
value = statusMap[value] || 'Applied'
}
row.push(value)
}
})
rowDataForPDF.push(row)
})
autoTable(doc, {
head: [headers],
body: rowDataForPDF,
styles: {
fontSize: 8,
cellPadding: 2
},
headStyles: {
fillColor: [41, 128, 185],
textColor: 255,
fontStyle: 'bold'
},
alternateRowStyles: {
fillColor: [245, 245, 245]
},
margin: { top: 20 }
})
doc.save(`data-table-${new Date().toISOString().split('T')[0]}.pdf`)
console.log('PDF exported successfully')
} catch (error) {
console.error('Error exporting PDF:', error)
}
}
</script>
<template>
<v-card class="elevation-2">
<DataTableHeader
:is-mobile="isMobile"
:is-tablet="isTablet"
:quick-filter="quickFilter"
@update:quick-filter="handleQuickFilterUpdate"
@export-csv="exportToCSV"
@export-excel="exportToExcel"
@export-pdf="exportToPDF"
/>
<v-divider />
<v-card-text class="pa-0">
<AgGridVue
class="vuetify-grid ag-theme-alpine-dark"
:style="`height:${isMobile ? '400px' : '600px'}; width:100%`"
:columnDefs="columnDefs"
:rowData="rowData"
:defaultColDef="defaultColDef"
:gridOptions="gridOptions"
@grid-ready="onGridReady"
@cell-value-changed="onCellValueChanged"
/>
</v-card-text>
<UserDetailsDialog
v-model="showDetailsDialog"
:selected-row-data="selectedRowData"
:is-mobile="isMobile"
@save-user="handleSaveUser"
/>
<ConfirmDeleteDialog
v-model="showDeleteDialog"
:selected-row-data="deleteRowData"
:is-mobile="isMobile"
@confirm="confirmDelete"
@cancel="cancelDelete"
/>
</v-card>
</template>

View File

@@ -0,0 +1,40 @@
<script setup>
import data from '@/views/demos/forms/tables/data-table/datatable'
const headers = [
{
title: 'ID',
sortable: false,
key: 'id',
},
{
title: 'NAME',
key: 'fullName',
},
{
title: 'EMAIL',
key: 'email',
},
{
title: 'DATE',
key: 'startDate',
},
{
title: 'EXPERIENCE',
key: 'experience',
},
{
title: 'AGE',
key: 'age',
},
]
</script>
<template>
<VDataTable
:headers="headers"
:items="data"
density="compact"
:items-per-page="5"
/>
</template>

View File

@@ -0,0 +1,119 @@
<script setup>
import data from '@/views/demos/forms/tables/data-table/datatable'
// Headers
const headers = [
{
title: '',
key: 'data-table-expand',
},
{
title: 'NAME',
key: 'fullName',
},
{
title: 'EMAIL',
key: 'email',
},
{
title: 'DATE',
key: 'startDate',
},
{
title: 'SALARY',
key: 'salary',
},
{
title: 'AGE',
key: 'age',
},
{
title: 'STATUS',
key: 'status',
},
]
const resolveStatusVariant = status => {
if (status === 1)
return {
color: 'primary',
text: 'Current',
}
else if (status === 2)
return {
color: 'success',
text: 'Professional',
}
else if (status === 3)
return {
color: 'error',
text: 'Rejected',
}
else if (status === 4)
return {
color: 'warning',
text: 'Resigned',
}
else
return {
color: 'info',
text: 'Applied',
}
}
</script>
<template>
<VDataTable
:headers="headers"
:items="data"
:items-per-page="5"
expand-on-click
>
<!-- Expanded Row Data -->
<template #expanded-row="slotProps">
<tr class="v-data-table__tr">
<td :colspan="headers.length">
<p class="my-1">
City: {{ slotProps.item.city }}
</p>
<p class="my-1">
Experience: {{ slotProps.item.experience }}
</p>
<p>Post: {{ slotProps.item.post }}</p>
</td>
</tr>
</template>
<!-- full name -->
<template #item.fullName="{ item }">
<div class="d-flex align-center">
<VAvatar
size="32"
:color="item.avatar ? '' : 'primary'"
:class="item.avatar ? '' : 'v-avatar-light-bg primary--text'"
:variant="!item.avatar ? 'tonal' : undefined"
>
<VImg
v-if="item.avatar"
:src="item.avatar"
/>
<span v-else>{{ avatarText(item.fullName) }}</span>
</VAvatar>
<div class="d-flex flex-column ms-3">
<span class="d-block font-weight-medium text-high-emphasis text-truncate">{{ item.fullName }}</span>
<small>{{ item.post }}</small>
</div>
</div>
</template>
<template #item.status="{ item }">
<VChip
:color="resolveStatusVariant(item.status).color"
class="font-weight-medium"
size="small"
>
{{ resolveStatusVariant(item.status).text }}
</VChip>
</template>
</VDataTable>
</template>

View File

@@ -0,0 +1,135 @@
<script setup>
import data from '@/views/demos/forms/tables/data-table/datatable'
const userList = ref([])
const options = ref({
page: 1,
itemsPerPage: 5,
sortBy: [''],
sortDesc: [false],
})
// headers
const headers = [
{
title: 'NAME',
key: 'fullName',
},
{
title: 'EMAIL',
key: 'email',
},
{
title: 'DATE',
key: 'startDate',
},
{
title: 'SALARY',
key: 'salary',
},
{
title: 'AGE',
key: 'age',
},
{
title: 'STATUS',
key: 'status',
},
]
const resolveStatusVariant = status => {
if (status === 1)
return {
color: 'primary',
text: 'Current',
}
else if (status === 2)
return {
color: 'success',
text: 'Professional',
}
else if (status === 3)
return {
color: 'error',
text: 'Rejected',
}
else if (status === 4)
return {
color: 'warning',
text: 'Resigned',
}
else
return {
color: 'info',
text: 'Applied',
}
}
onMounted(() => {
userList.value = JSON.parse(JSON.stringify(data))
})
</script>
<template>
<VDataTable
:headers="headers"
:items="userList"
:items-per-page="options.itemsPerPage"
:page="options.page"
:options="options"
>
<!-- full name -->
<template #item.fullName="{ item }">
<div class="d-flex align-center">
<VAvatar
size="32"
:color="item.avatar ? '' : 'primary'"
:class="item.avatar ? '' : 'v-avatar-light-bg primary--text'"
:variant="!item.avatar ? 'tonal' : undefined"
>
<VImg
v-if="item.avatar"
:src="item.avatar"
/>
<span v-else>{{ avatarText(item.fullName) }}</span>
</VAvatar>
<div class="d-flex flex-column ms-3">
<span class="d-block font-weight-medium text-high-emphasis text-truncate">{{ item.fullName }}</span>
<small>{{ item.post }}</small>
</div>
</div>
</template>
<!-- status -->
<template #item.status="{ item }">
<VChip
:color="resolveStatusVariant(item.status).color"
class="font-weight-medium"
size="small"
>
{{ resolveStatusVariant(item.status).text }}
</VChip>
</template>
<template #bottom>
<VCardText class="pt-2">
<div class="d-flex flex-wrap justify-center justify-sm-space-between gap-y-2 mt-2">
<VSelect
v-model="options.itemsPerPage"
:items="[5, 10, 25, 50, 100]"
label="Rows per page:"
variant="underlined"
style="max-inline-size: 8rem;min-inline-size: 5rem;"
/>
<VPagination
v-model="options.page"
:total-visible="$vuetify.display.smAndDown ? 3 : 5"
:length="Math.ceil(userList.length / options.itemsPerPage)"
/>
</div>
</VCardText>
</template>
</VDataTable>
</template>

View File

@@ -0,0 +1,101 @@
<script setup>
import data from '@/views/demos/forms/tables/data-table/datatable'
const headers = [
{
title: 'NAME',
key: 'fullName',
},
{
title: 'EMAIL',
key: 'email',
},
{
title: 'DATE',
key: 'startDate',
},
{
title: 'SALARY',
key: 'salary',
},
{
title: 'AGE',
key: 'age',
},
{
title: 'STATUS',
key: 'status',
},
]
const resolveStatusVariant = status => {
if (status === 1)
return {
color: 'primary',
text: 'Current',
}
else if (status === 2)
return {
color: 'success',
text: 'Professional',
}
else if (status === 3)
return {
color: 'error',
text: 'Rejected',
}
else if (status === 4)
return {
color: 'warning',
text: 'Resigned',
}
else
return {
color: 'info',
text: 'Applied',
}
}
</script>
<template>
<VDataTable
:headers="headers"
:items="data"
:items-per-page="10"
height="300"
fixed-header
>
<!-- full name -->
<template #item.fullName="{ item }">
<div class="d-flex align-center">
<VAvatar
size="32"
:color="item.avatar ? '' : 'primary'"
:class="item.avatar ? '' : 'v-avatar-light-bg primary--text'"
:variant="!item.avatar ? 'tonal' : undefined"
>
<VImg
v-if="item.avatar"
:src="item.avatar"
/>
<span v-else>{{ avatarText(item.fullName) }}</span>
</VAvatar>
<div class="d-flex flex-column ms-3">
<span class="d-block font-weight-medium text-high-emphasis text-truncate">{{ item.fullName }}</span>
<small>{{ item.post }}</small>
</div>
</div>
</template>
<!-- status -->
<template #item.status="{ item }">
<VChip
:color="resolveStatusVariant(item.status).color"
class="font-weight-medium"
size="small"
>
{{ resolveStatusVariant(item.status).text }}
</VChip>
</template>
</VDataTable>
</template>

View File

@@ -0,0 +1,398 @@
<script setup>
import avatar1 from '@images/avatars/avatar-1.png'
import avatar2 from '@images/avatars/avatar-2.png'
import avatar4 from '@images/avatars/avatar-4.png'
import avatar7 from '@images/avatars/avatar-7.png'
import avatar8 from '@images/avatars/avatar-8.png'
const userList = [
{
responsiveId: '',
id: 1,
avatar: avatar8,
fullName: 'Korrie O\'Crevy',
post: 'Nuclear Power Engineer',
email: 'kocrevy0@thetimes.co.uk',
city: 'Krasnosilka',
startDate: '09/23/2016',
salary: 23896.35,
age: '61',
experience: '1 Year',
status: 'Professional',
},
{
responsiveId: '',
id: 2,
avatar: avatar1,
fullName: 'Bailie Coulman',
post: 'VP Quality Control',
email: 'bcoulman1@yolasite.com',
city: 'Hinigaran',
startDate: '05/20/2018',
salary: 13633.69,
age: '63',
experience: '3 Years',
status: 'Professional',
},
{
responsiveId: '',
id: 3,
avatar: avatar7,
fullName: 'Stella Ganderton',
post: 'Operator',
email: 'sganderton2@tuttocitta.it',
city: 'Golcowa',
startDate: '03/24/2018',
salary: 13076.28,
age: '66',
experience: '6 Years',
status: 'Applied',
},
{
responsiveId: '',
id: 4,
avatar: avatar8,
fullName: 'Dorolice Crossman',
post: 'Cost Accountant',
email: 'dcrossman3@google.co.jp',
city: 'Paquera',
startDate: '12/03/2017',
salary: 12336.17,
age: '22',
experience: '2 Years',
status: 'Professional',
},
{
responsiveId: '',
id: 6,
avatar: '',
fullName: 'Genevra Honeywood',
post: 'Geologist',
email: 'ghoneywood5@narod.ru',
city: 'Maofan',
startDate: '06/01/2017',
salary: 17803.8,
age: '61',
experience: '1 Year',
status: 'Current',
},
{
responsiveId: '',
id: 7,
avatar: '',
fullName: 'Eileen Diehn',
post: 'Environmental Specialist',
email: 'ediehn6@163.com',
city: 'Lampuyang',
startDate: '10/15/2017',
salary: 18991.67,
age: '59',
experience: '9 Years',
status: 'Rejected',
},
{
responsiveId: '',
id: 8,
avatar: avatar7,
fullName: 'Richardo Aldren',
post: 'Senior Sales Associate',
email: 'raldren7@mtv.com',
city: 'Skoghall',
startDate: '11/05/2016',
salary: 19230.13,
age: '55',
experience: '5 Years',
status: 'Rejected',
},
{
responsiveId: '',
id: 9,
avatar: avatar2,
fullName: 'Allyson Moakler',
post: 'Safety Technician',
email: 'amoakler8@shareasale.com',
city: 'Mogilany',
startDate: '12/29/2018',
salary: 11677.32,
age: '39',
experience: '9 Years',
status: 'Applied',
},
{
responsiveId: '',
id: 11,
avatar: '',
fullName: 'De Falloon',
post: 'Sales Representative',
email: 'dfalloona@ifeng.com',
city: 'Colima',
startDate: '06/12/2018',
salary: 19252.12,
age: '30',
experience: '0 Year',
status: 'Resigned',
},
{
responsiveId: '',
id: 12,
avatar: '',
fullName: 'Cyrus Gornal',
post: 'Senior Sales Associate',
email: 'cgornalb@fda.gov',
city: 'Boro Utara',
startDate: '12/09/2017',
salary: 16745.47,
age: '22',
experience: '2 Years',
status: 'Resigned',
},
{
responsiveId: '',
id: 13,
avatar: '',
fullName: 'Tallou Balf',
post: 'Staff Accountant',
email: 'tbalfc@sina.com.cn',
city: 'Siliana',
startDate: '01/21/2016',
salary: 15488.53,
age: '36',
experience: '6 Years',
status: 'Resigned',
},
{
responsiveId: '',
id: 15,
avatar: '',
fullName: 'Wilmar Bourton',
post: 'Administrative Assistant',
email: 'wbourtone@sakura.ne.jp',
city: 'Bích Động',
startDate: '04/25/2018',
salary: 13304.45,
age: '19',
experience: '9 Years',
status: 'Applied',
},
{
responsiveId: '',
id: 16,
avatar: avatar4,
fullName: 'Robinson Brazenor',
post: 'General Manager',
email: 'rbrazenorf@symantec.com',
city: 'Gendiwu',
startDate: '12/23/2017',
salary: 11953.08,
age: '66',
experience: '6 Years',
status: 'Applied',
},
{
responsiveId: '',
id: 17,
avatar: '',
fullName: 'Nadia Bettenson',
post: 'Environmental Tech',
email: 'nbettensong@joomla.org',
city: 'Chabařovice',
startDate: '07/11/2018',
salary: 20484.44,
age: '64',
experience: '4 Years',
status: 'Current',
},
{
responsiveId: '',
id: 18,
avatar: '',
fullName: 'Titus Hayne',
post: 'Web Designer',
email: 'thayneh@kickstarter.com',
city: 'Yangon',
startDate: '05/25/2019',
salary: 16871.48,
age: '59',
experience: '9 Years',
status: 'Current',
},
{
responsiveId: '',
id: 19,
avatar: avatar4,
fullName: 'Roxie Huck',
post: 'Administrative Assistant',
email: 'rhucki@ed.gov',
city: 'Polýkastro',
startDate: '04/04/2019',
salary: 19653.56,
age: '41',
experience: '1 Year',
status: 'Resigned',
},
{
responsiveId: '',
id: 23,
avatar: avatar7,
fullName: 'Rosmunda Steed',
post: 'Assistant Media Planner',
email: 'rsteedm@xing.com',
city: 'Manzanares',
startDate: '12/23/2017',
salary: 13778.34,
age: '21',
experience: '1 Year',
status: 'Applied',
},
{
responsiveId: '',
id: 26,
avatar: avatar2,
fullName: 'Morgen Benes',
post: 'Senior Sales Associate',
email: 'mbenesp@ted.com',
city: 'Cà Mau',
startDate: '04/10/2016',
salary: 16969.63,
age: '42',
experience: '2 Years',
status: 'Resigned',
},
{
responsiveId: '',
id: 28,
avatar: '',
fullName: 'Kliment McGinney',
post: 'Chief Design Engineer',
email: 'kmcginneyr@paginegialle.it',
city: 'Xiaocheng',
startDate: '07/09/2018',
salary: 24027.81,
age: '28',
experience: '8 Years',
status: 'Resigned',
},
{
responsiveId: '',
id: 31,
avatar: '',
fullName: 'Teressa Bleakman',
post: 'Senior Editor',
email: 'tbleakmanu@phpbb.com',
city: 'Žebrák',
startDate: '09/03/2016',
salary: 24875.41,
age: '37',
experience: '7 Years',
status: 'Applied',
},
]
const headers = [
{
title: 'Group by status',
key: 'data-table-group',
},
{
title: 'NAME',
key: 'fullName',
},
{
title: 'EMAIL',
key: 'email',
},
{
title: 'DATE',
key: 'startDate',
},
{
title: 'SALARY',
key: 'salary',
},
{
title: 'AGE',
key: 'age',
},
{
title: 'STATUS',
key: 'status',
},
]
const groupBy = [{ key: 'status' }]
const resolveStatusVariant = status => {
if (status === 'Current')
return { color: 'primary' }
else if (status === 'Professional')
return { color: 'success' }
else if (status === 'Rejected')
return { color: 'error' }
else if (status === 'Resigned')
return { color: 'warning' }
else
return { color: 'info' }
}
const getIcon = props => props.icon
</script>
<template>
<VDataTable
:headers="headers"
:items="userList"
:items-per-page="10"
:group-by="groupBy"
>
<!-- full name -->
<template #item.fullName="{ item }">
<div class="d-flex align-center">
<VAvatar
size="32"
:color="item.avatar ? '' : 'primary'"
:class="item.avatar ? '' : 'v-avatar-light-bg primary--text'"
:variant="!item.avatar ? 'tonal' : undefined"
>
<VImg
v-if="item.avatar"
:src="item.avatar"
/>
<span v-else>{{ avatarText(item.fullName) }}</span>
</VAvatar>
<div class="d-flex flex-column ms-3">
<span class="d-block font-weight-medium text-high-emphasis text-truncate">{{ item.fullName }}</span>
<small>{{ item.post }}</small>
</div>
</div>
</template>
<template #item.status="{ item }">
<VChip
:color="resolveStatusVariant(item.status).color"
size="small"
class="font-weight-medium"
>
{{ item.status }}
</VChip>
</template>
<template #data-table-group="{ props, item, count }">
<td>
<VBtn
v-bind="props"
variant="text"
density="comfortable"
>
<VIcon
class="flip-in-rtl"
:icon="getIcon(props)"
/>
</VBtn>
<span>{{ item.value }}</span>
<span>({{ count }})</span>
</td>
</template>
</VDataTable>
</template>

View File

@@ -0,0 +1,305 @@
<script setup>
const {
data: productList,
error,
} = await useApi('pages/datatable')
const search = ref('')
// headers
const headers = [
{
title: 'PRODUCT',
key: 'product.name',
},
{
title: 'DATE',
key: 'date',
},
{
title: 'CATEGORY',
key: 'product.category',
},
{
title: 'BUYERS',
key: 'buyer.name',
},
{
title: 'PAYMENT',
key: 'payment',
sortable: false,
},
{
title: 'STATUS',
key: 'status',
sortable: false,
},
{
title: 'DELETE',
key: 'delete',
sortable: false,
},
]
const deleteItem = itemId => {
if (!productList.value)
return
const index = productList.value.findIndex(item => item.product.id === itemId)
productList.value.splice(index, 1)
}
const categoryIcons = [
{
name: 'Mouse',
icon: 'tabler-mouse',
color: 'warning',
},
{
name: 'Glass',
icon: 'tabler-eyeglass',
color: 'primary',
},
{
name: 'Smart Watch',
icon: 'tabler-device-watch',
color: 'success',
},
{
name: 'Bag',
icon: 'tabler-briefcase',
color: 'info',
},
{
name: 'Storage Device',
icon: 'tabler-device-audio-tape',
color: 'warning',
},
{
name: 'Bluetooth',
icon: 'tabler-bluetooth',
color: 'error',
},
{
name: 'Gaming',
icon: 'tabler-device-gamepad-2',
color: 'warning',
},
{
name: 'Home',
icon: 'tabler-home',
color: 'error',
},
{
name: 'VR',
icon: 'tabler-badge-vr',
color: 'primary',
},
{
name: 'Shoes',
icon: 'tabler-shoe',
color: 'success',
},
{
name: 'Electronics',
icon: 'tabler-cpu',
color: 'info',
},
{
name: 'Projector',
icon: 'tabler-theater',
color: 'warning',
},
{
name: 'iPod',
icon: 'tabler-device-airpods',
color: 'error',
},
{
name: 'Keyboard',
icon: 'tabler-keyboard',
color: 'primary',
},
{
name: 'Smart Phone',
icon: 'tabler-device-mobile',
color: 'success',
},
{
name: 'Smart TV',
icon: 'tabler-device-tv',
color: 'info',
},
{
name: 'Google Home',
icon: 'tabler-brand-google',
color: 'warning',
},
{
name: 'Mac',
icon: 'tabler-brand-apple',
color: 'error',
},
{
name: 'Headphone',
icon: 'tabler-headphones',
color: 'primary',
},
{
name: 'iMac',
icon: 'tabler-device-imac',
color: 'success',
},
{
name: 'iPhone',
icon: 'tabler-brand-apple',
color: 'warning',
},
]
const resolveStatusColor = status => {
if (status === 'Confirmed')
return 'primary'
if (status === 'Completed')
return 'success'
if (status === 'Cancelled')
return 'error'
}
const categoryIconFilter = categoryName => {
const index = categoryIcons.findIndex(category => category.name === categoryName)
if (index !== -1)
return [{
icon: categoryIcons[index].icon,
color: categoryIcons[index].color,
}]
return [{
icon: 'tabler-help-circle',
color: 'primary',
}]
}
if (error.value)
console.error(error.value)
</script>
<template>
<div>
<VCardText>
<VRow>
<VCol
cols="12"
offset-md="8"
md="4"
>
<AppTextField
v-model="search"
placeholder="Search ..."
append-inner-icon="tabler-search"
single-line
hide-details
dense
outlined
/>
</VCol>
</VRow>
</VCardText>
<!-- 👉 Data Table -->
<VDataTable
:headers="headers"
:items="productList || []"
:search="search"
:items-per-page="5"
class="text-no-wrap"
>
<!-- product -->
<template #item.product.name="{ item }">
<div class="d-flex align-center">
<div>
<VImg
:src="item.product.image"
height="40"
width="40"
/>
</div>
<div class="d-flex flex-column ms-3">
<span class="d-block font-weight-medium text-truncate text-high-emphasis">{{ item.product.name }}</span>
<span class="text-xs">{{ item.product.brand }}</span>
</div>
</div>
</template>
<!-- category -->
<template #item.product.category="{ item }">
<div class="d-flex align-center">
<VAvatar
v-for="(category, index) in categoryIconFilter(item.product.category)"
:key="index"
size="26"
:color="category.color"
variant="tonal"
>
<VIcon
size="20"
:color="category.color"
class="rounded-0"
>
{{ category.icon }}
</VIcon>
</VAvatar>
<span class="ms-1 text-no-wrap">{{ item.product.category }}</span>
</div>
</template>
<!-- buyer -->
<template #item.buyer.name="{ item }">
<div class="d-flex align-center">
<VAvatar
size="1.875rem"
:color="!item.buyer.avatar ? 'primary' : undefined"
:variant="!item.buyer.avatar ? 'tonal' : undefined"
>
<VImg
v-if="item.buyer.avatar"
:src="item.buyer.avatar"
/>
<span v-else>{{ item.buyer.name.slice(0, 2).toUpperCase() }}</span>
</VAvatar>
<span class="text-no-wrap font-weight-medium text-high-emphasis ms-2">{{ item.buyer.name }}</span>
</div>
</template>
<!-- Payment -->
<template #item.payment="{ item }">
<div class="d-flex flex-column">
<div class="d-flex align-center">
<span class="text-high-emphasis font-weight-medium">${{ item.payment.paidAmount }}</span>
<span v-if="item.payment.paidAmount !== item.payment.total">/{{ item.payment.total }}</span>
</div>
<span class="text-xs text-no-wrap">{{ item.payment.receivedPaymentStatus }}</span>
</div>
</template>
<!-- Status -->
<template #item.status="{ item }">
<VChip
:color="resolveStatusColor(item.payment.status)"
:class="`text-${resolveStatusColor(item.payment.status)}`"
size="small"
class="font-weight-medium"
>
{{ item.payment.status }}
</VChip>
</template>
<!-- Delete -->
<template #item.delete="{ item }">
<IconBtn @click="deleteItem(item.product.id)">
<VIcon icon="tabler-trash" />
</IconBtn>
</template>
</VDataTable>
</div>
</template>

View File

@@ -0,0 +1,338 @@
<script setup>
import data from '@/views/demos/forms/tables/data-table/datatable'
const editDialog = ref(false)
const deleteDialog = ref(false)
const defaultItem = ref({
responsiveId: '',
id: -1,
avatar: '',
fullName: '',
post: '',
email: '',
city: '',
startDate: '',
salary: -1,
age: '',
experience: '',
status: -1,
})
const editedItem = ref(defaultItem.value)
const editedIndex = ref(-1)
const userList = ref([])
// status options
const selectedOptions = [
{
text: 'Current',
value: 1,
},
{
text: 'Professional',
value: 2,
},
{
text: 'Rejected',
value: 3,
},
{
text: 'Resigned',
value: 4,
},
{
text: 'Applied',
value: 5,
},
]
// headers
const headers = [
{
title: 'NAME',
key: 'fullName',
},
{
title: 'EMAIL',
key: 'email',
},
{
title: 'DATE',
key: 'startDate',
},
{
title: 'SALARY',
key: 'salary',
},
{
title: 'AGE',
key: 'age',
},
{
title: 'STATUS',
key: 'status',
},
{
title: 'ACTIONS',
key: 'actions',
},
]
const resolveStatusVariant = status => {
if (status === 1)
return {
color: 'primary',
text: 'Current',
}
else if (status === 2)
return {
color: 'success',
text: 'Professional',
}
else if (status === 3)
return {
color: 'error',
text: 'Rejected',
}
else if (status === 4)
return {
color: 'warning',
text: 'Resigned',
}
else
return {
color: 'info',
text: 'Applied',
}
}
const editItem = item => {
editedIndex.value = userList.value.indexOf(item)
editedItem.value = { ...item }
editDialog.value = true
}
const deleteItem = item => {
editedIndex.value = userList.value.indexOf(item)
editedItem.value = { ...item }
deleteDialog.value = true
}
const close = () => {
editDialog.value = false
editedIndex.value = -1
editedItem.value = { ...defaultItem.value }
}
const closeDelete = () => {
deleteDialog.value = false
editedIndex.value = -1
editedItem.value = { ...defaultItem.value }
}
const save = () => {
if (editedIndex.value > -1)
Object.assign(userList.value[editedIndex.value], editedItem.value)
else
userList.value.push(editedItem.value)
close()
}
const deleteItemConfirm = () => {
userList.value.splice(editedIndex.value, 1)
closeDelete()
}
onMounted(() => {
userList.value = JSON.parse(JSON.stringify(data))
})
</script>
<template>
<!-- 👉 Datatable -->
<VDataTable
:headers="headers"
:items="userList"
:items-per-page="5"
>
<!-- full name -->
<template #item.fullName="{ item }">
<div class="d-flex align-center">
<!-- avatar -->
<VAvatar
size="32"
:color="item.avatar ? '' : 'primary'"
:class="item.avatar ? '' : 'v-avatar-light-bg primary--text'"
:variant="!item.avatar ? 'tonal' : undefined"
>
<VImg
v-if="item.avatar"
:src="item.avatar"
/>
<span v-else>{{ avatarText(item.fullName) }}</span>
</VAvatar>
<div class="d-flex flex-column ms-3">
<span class="d-block font-weight-medium text-high-emphasis text-truncate">{{ item.fullName }}</span>
<small>{{ item.post }}</small>
</div>
</div>
</template>
<!-- status -->
<template #item.status="{ item }">
<VChip
:color="resolveStatusVariant(item.status).color"
size="small"
>
{{ resolveStatusVariant(item.status).text }}
</VChip>
</template>
<!-- Actions -->
<template #item.actions="{ item }">
<div class="d-flex gap-1">
<IconBtn @click="editItem(item)">
<VIcon icon="tabler-edit" />
</IconBtn>
<IconBtn @click="deleteItem(item)">
<VIcon icon="tabler-trash" />
</IconBtn>
</div>
</template>
</VDataTable>
<!-- 👉 Edit Dialog -->
<VDialog
v-model="editDialog"
max-width="600px"
>
<VCard title="Edit Item">
<VCardText>
<div class="text-body-1 mb-6">
Name: <span class="text-h6">{{ editedItem?.fullName }}</span>
</div>
<VRow>
<!-- fullName -->
<VCol
cols="12"
sm="6"
>
<AppTextField
v-model="editedItem.fullName"
label="User name"
/>
</VCol>
<!-- email -->
<VCol
cols="12"
sm="6"
>
<AppTextField
v-model="editedItem.email"
label="Email"
/>
</VCol>
<!-- salary -->
<VCol
cols="12"
sm="6"
>
<AppTextField
v-model="editedItem.salary"
label="Salary"
prefix="$"
type="number"
/>
</VCol>
<!-- age -->
<VCol
cols="12"
sm="6"
>
<AppTextField
v-model="editedItem.age"
label="Age"
type="number"
/>
</VCol>
<!-- start date -->
<VCol
cols="12"
sm="6"
>
<AppTextField
v-model="editedItem.startDate"
label="Date"
/>
</VCol>
<!-- status -->
<VCol
cols="12"
sm="6"
>
<AppSelect
v-model="editedItem.status"
:items="selectedOptions"
item-title="text"
item-value="value"
label="Standard"
/>
</VCol>
</VRow>
</VCardText>
<VCardText>
<div class="self-align-end d-flex gap-4 justify-end">
<VBtn
color="error"
variant="outlined"
@click="close"
>
Cancel
</VBtn>
<VBtn
color="success"
variant="elevated"
@click="save"
>
Save
</VBtn>
</div>
</VCardText>
</VCard>
</VDialog>
<!-- 👉 Delete Dialog -->
<VDialog
v-model="deleteDialog"
max-width="500px"
>
<VCard title="Are you sure you want to delete this item?">
<VCardText>
<div class="d-flex justify-center gap-4">
<VBtn
color="error"
variant="outlined"
@click="closeDelete"
>
Cancel
</VBtn>
<VBtn
color="success"
variant="elevated"
@click="deleteItemConfirm"
>
OK
</VBtn>
</div>
</VCardText>
</VCard>
</VDialog>
</template>

View File

@@ -0,0 +1,100 @@
<script setup>
import data from '@/views/demos/forms/tables/data-table/datatable'
const headers = [
{
title: 'NAME',
key: 'fullName',
},
{
title: 'EMAIL',
key: 'email',
},
{
title: 'DATE',
key: 'startDate',
},
{
title: 'SALARY',
key: 'salary',
},
{
title: 'AGE',
key: 'age',
},
{
title: 'STATUS',
key: 'status',
},
]
const resolveStatusVariant = status => {
if (status === 1)
return {
color: 'primary',
text: 'Current',
}
else if (status === 2)
return {
color: 'success',
text: 'Professional',
}
else if (status === 3)
return {
color: 'error',
text: 'Rejected',
}
else if (status === 4)
return {
color: 'warning',
text: 'Resigned',
}
else
return {
color: 'info',
text: 'Applied',
}
}
</script>
<template>
<VDataTable
:headers="headers"
:items="data"
:items-per-page="5"
show-select
>
<!-- full name -->
<template #item.fullName="{ item }">
<div class="d-flex align-center">
<VAvatar
size="32"
:color="item.avatar ? '' : 'primary'"
:class="item.avatar ? '' : 'v-avatar-light-bg primary--text'"
:variant="!item.avatar ? 'tonal' : undefined"
>
<VImg
v-if="item.avatar"
:src="item.avatar"
/>
<span v-else>{{ avatarText(item.fullName) }}</span>
</VAvatar>
<div class="d-flex flex-column ms-3">
<span class="d-block font-weight-medium text-high-emphasis text-truncate">{{ item.fullName }}</span>
<small>{{ item.post }}</small>
</div>
</div>
</template>
<!-- status -->
<template #item.status="{ item }">
<VChip
:color="resolveStatusVariant(item.status).color"
class="font-weight-medium"
size="small"
>
{{ resolveStatusVariant(item.status).text }}
</VChip>
</template>
</VDataTable>
</template>

View File

@@ -0,0 +1,143 @@
<template>
<v-dialog
:model-value="modelValue"
@update:model-value="handleClose"
:max-width="isMobile ? '95%' : '500px'"
persistent
>
<v-card>
<v-card-title class="d-flex align-center">
<VIcon icon="tabler-alert-triangle" color="error" class="me-3" />
<span class="text-h6">Confirm Delete</span>
</v-card-title>
<v-divider />
<v-card-text class="py-6">
<div class="text-body-1 mb-4">
Are you sure you want to delete this user?
</div>
<v-card
v-if="selectedRowData"
variant="outlined"
class="pa-3 bg-grey-lighten-5"
>
<div class="d-flex flex-column gap-2">
<div class="d-flex align-center">
<VIcon icon="tabler-user" size="small" class="me-2" />
<strong class="me-2">Name:</strong>
{{ selectedRowData.fullName || 'Unknown' }}
</div>
<div class="d-flex align-center">
<VIcon icon="tabler-mail" size="small" class="me-2" />
<strong class="me-2">Email:</strong>
{{ selectedRowData.email || 'Unknown' }}
</div>
</div>
</v-card>
<v-alert
type="warning"
variant="tonal"
class="mt-4"
>
<template #prepend>
<VIcon icon="tabler-alert-circle" />
</template>
<template #text>
This action cannot be undone and will permanently delete the user data.
</template>
</v-alert>
</v-card-text>
<v-divider />
<v-card-actions class="pa-4">
<v-spacer />
<v-btn
variant="outlined"
color="grey"
@click="handleCancel"
:class="isMobile ? 'flex-grow-1 me-2' : ''"
>
<template #prepend>
<VIcon icon="tabler-x" />
</template>
Cancel
</v-btn>
<v-btn
variant="flat"
color="error"
@click="handleConfirm"
:class="isMobile ? 'flex-grow-1' : ''"
>
<template #prepend>
<VIcon icon="tabler-trash" />
</template>
Delete
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup>
defineProps({
modelValue: {
type: Boolean,
default: false
},
selectedRowData: {
type: Object,
default: null
},
isMobile: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:model-value', 'confirm', 'cancel'])
const handleClose = (value) => {
if (!value) {
emit('update:model-value', false)
emit('cancel')
}
}
const handleCancel = () => {
emit('update:model-value', false)
emit('cancel')
}
const handleConfirm = () => {
emit('update:model-value', false)
emit('confirm')
}
</script>
<style scoped>
.v-card-title {
background: rgba(var(--v-theme-error), 0.1);
border-bottom: 1px solid rgba(var(--v-theme-error), 0.2);
}
.v-alert {
border-left: 4px solid rgb(var(--v-theme-warning));
}
@media (max-width: 600px) {
.v-card-actions {
flex-direction: column;
gap: 8px;
}
.v-btn {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,113 @@
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
isMobile: Boolean,
isTablet: Boolean,
quickFilter: String
})
const emit = defineEmits(['update:quick-filter', 'export-csv', 'export-excel', 'export-pdf'])
const updateQuickFilter = value => {
emit('update:quick-filter', value)
}
const handleExportCsv = () => emit('export-csv')
const handleExportExcel = () => emit('export-excel')
const handleExportPdf = () => emit('export-pdf')
</script>
<template>
<v-card-title
class="d-flex align-center py-4 px-4"
:class="{
'flex-column ga-4': isMobile,
'flex-row': !isMobile
}"
>
<div class="d-flex align-center">
<span class="text-h6 font-weight-medium">Data Table</span>
</div>
<v-spacer v-if="!isMobile" />
<div
class="d-flex align-center"
:class="{
'flex-column ga-3 w-100': isMobile,
'ga-3': !isMobile
}"
>
<v-text-field
:model-value="quickFilter"
@update:model-value="updateQuickFilter"
append-inner-icon="tabler-search"
label="Search..."
single-line
hide-details
density="compact"
:style="getSearchFieldWidth"
variant="outlined"
clearable
placeholder="Search in all columns..."
/>
<div
class="d-flex ga-2"
:class="{ 'w-100': isMobile }"
>
<v-btn
color="success"
variant="outlined"
size="small"
@click="handleExportCsv"
:class="{ 'flex-1-1-0': isMobile }"
>
<VIcon icon="tabler-file-text" size="18" />
<span v-if="!isMobile" class="ms-2">Export CSV</span>
<span v-else-if="isTablet" class="ms-1">CSV</span>
<span v-else class="ms-1 text-caption">CSV</span>
</v-btn>
<v-btn
color="primary"
variant="outlined"
size="small"
@click="handleExportExcel"
:class="{ 'flex-1-1-0': isMobile }"
>
<VIcon icon="tabler-file-spreadsheet" size="18" />
<span v-if="!isMobile" class="ms-2">Export Excel</span>
<span v-else-if="isTablet" class="ms-1">Excel</span>
<span v-else class="ms-1 text-caption">Excel</span>
</v-btn>
<v-btn
color="error"
variant="outlined"
size="small"
@click="handleExportPdf"
:class="{ 'flex-1-1-0': isMobile }"
>
<VIcon icon="tabler-file-filled" size="18" />
<span v-if="!isMobile" class="ms-2">Export PDF</span>
<span v-else-if="isTablet" class="ms-1">PDF</span>
<span v-else class="ms-1 text-caption">PDF</span>
</v-btn>
</div>
</div>
</v-card-title>
</template>
<script>
export default {
computed: {
getSearchFieldWidth() {
if (this.isMobile) return 'width: 100%'
if (this.isTablet) return 'width: 400px'
return 'width: 500px'
}
}
}
</script>

View File

@@ -0,0 +1,430 @@
<script setup>
const props = defineProps({
modelValue: Boolean,
selectedRowData: Object,
isMobile: Boolean
})
const emit = defineEmits(['update:modelValue', 'save-user'])
const isEditMode = ref(false)
const editedData = ref({})
watch(() => props.modelValue, (newVal) => {
if (newVal && props.selectedRowData) {
editedData.value = { ...props.selectedRowData }
}
})
const handleEnterKey = () => {
if (isEditMode.value) {
saveChanges()
}
}
const closeDialog = () => {
isEditMode.value = false
emit('update:modelValue', false)
}
const toggleEditMode = () => {
if (!isEditMode.value) {
editedData.value = { ...props.selectedRowData }
}
isEditMode.value = !isEditMode.value
}
const saveChanges = () => {
emit('save-user', editedData.value)
isEditMode.value = false
}
const cancelEdit = () => {
editedData.value = { ...props.selectedRowData }
isEditMode.value = false
}
const formatDate = (dateValue) => {
if (!dateValue) return 'N/A'
const date = new Date(dateValue)
if (isNaN(date.getTime())) return 'Invalid Date'
const day = date.getDate().toString().padStart(2, '0')
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const year = date.getFullYear()
return `${day}/${month}/${year}`
}
const formatDateForInput = (dateValue) => {
if (!dateValue) return ''
const date = new Date(dateValue)
if (isNaN(date.getTime())) return ''
return date.toISOString().split('T')[0]
}
const formatSalary = (salaryValue) => {
if (!salaryValue) return 'N/A'
const intValue = Math.floor(Number(salaryValue))
return `$${intValue.toLocaleString()}`
}
const getStatusLabel = (statusValue) => {
const statusMap = {
0: 'Applied',
1: 'Current',
2: 'Professional',
3: 'Rejected',
4: 'Resigned'
}
return statusMap[statusValue] || 'Applied'
}
const getStatusColor = (statusValue) => {
const colorMap = {
0: 'info',
1: 'primary',
2: 'success',
3: 'error',
4: 'warning'
}
return colorMap[statusValue] || 'grey'
}
const statusOptions = [
{ value: 0, title: 'Applied', color: 'info' },
{ value: 1, title: 'Current', color: 'primary' },
{ value: 2, title: 'Professional', color: 'success' },
{ value: 3, title: 'Rejected', color: 'error' },
{ value: 4, title: 'Resigned', color: 'warning' }
]
const getFieldIcon = (field) => {
const iconMap = {
name: 'tabler-user',
email: 'tabler-mail',
startDate: 'tabler-calendar',
salary: 'tabler-currency-dollar',
age: 'tabler-hourglass',
status: 'tabler-badge'
}
return iconMap[field] || 'tabler-info-circle'
}
</script>
<template>
<v-dialog :model-value="modelValue" @update:model-value="closeDialog" :max-width="isMobile ? '95%' : '700px'" persistent>
<v-card
class="elevation-12"
rounded="lg"
:style="{
background: 'rgba(var(--v-theme-surface), 0.15)',
backdropFilter: 'blur(20px)',
webkitBackdropFilter: 'blur(20px)',
border: '1px solid rgba(var(--v-theme-outline), 0.2)',
boxShadow: '0 25px 45px rgba(0,0,0,0.1)'
}"
>
<v-card-title class="bg-primary text-white pa-4">
<div class="d-flex justify-space-between align-center w-100">
<div class="d-flex align-center">
<v-icon icon="tabler-user-circle" size="28" class="me-3" />
<span class="text-h5">{{ isEditMode ? 'Edit User' : 'User Details' }}</span>
</div>
<div class="d-flex align-center gap-2">
<template v-if="!isEditMode">
<v-btn
icon="tabler-edit"
variant="text"
size="small"
color="white"
:disabled="!selectedRowData"
@click="toggleEditMode"
title="Edit"
/>
<v-btn
icon="tabler-x"
variant="text"
size="small"
color="white"
@click="closeDialog"
title="Close"
/>
</template>
<template v-else>
<v-btn
icon="tabler-x"
variant="text"
size="small"
color="white"
@click="cancelEdit"
title="Cancel"
/>
<v-btn
icon="tabler-check"
variant="text"
size="small"
color="white"
@click="saveChanges"
title="Save"
/>
</template>
</div>
</div>
</v-card-title>
<v-card-text class="pa-6" v-if="selectedRowData">
<v-form @keyup.enter="handleEnterKey">
<v-container fluid class="pa-0">
<v-row>
<v-col cols="12" sm="6" class="mb-2">
<v-card variant="outlined" class="pa-4 h-100" color="surface-variant">
<div class="d-flex align-center">
<v-avatar
class="me-4"
size="40"
:style="{
background: 'rgba(var(--v-theme-primary), 0.2)',
backdropFilter: 'blur(10px)',
webkitBackdropFilter: 'blur(10px)',
border: '1px solid rgba(var(--v-theme-primary), 0.3)'
}"
>
<v-icon :icon="getFieldIcon('name')" color="primary" />
</v-avatar>
<div class="flex-grow-1">
<v-card-subtitle class="pa-0 text-caption text-medium-emphasis">Full Name</v-card-subtitle>
<v-card-title v-if="!isEditMode" class="pa-0 text-h6 text-high-emphasis">
{{ selectedRowData.fullName || 'N/A' }}
</v-card-title>
<v-text-field
v-else
v-model="editedData.fullName"
variant="outlined"
density="compact"
hide-details
class="mt-1"
/>
</div>
</div>
</v-card>
</v-col>
<v-col cols="12" sm="6" class="mb-2">
<v-card variant="outlined" class="pa-4 h-100" color="surface-variant">
<div class="d-flex align-center">
<v-avatar
class="me-4"
size="40"
:style="{
background: 'rgba(var(--v-theme-secondary), 0.2)',
backdropFilter: 'blur(10px)',
webkitBackdropFilter: 'blur(10px)',
border: '1px solid rgba(var(--v-theme-secondary), 0.3)'
}"
>
<v-icon :icon="getFieldIcon('email')" color="secondary" />
</v-avatar>
<div class="flex-grow-1">
<v-card-subtitle class="pa-0 text-caption text-medium-emphasis">Email Address</v-card-subtitle>
<v-card-title v-if="!isEditMode" class="pa-0 text-body-1 text-high-emphasis">
{{ selectedRowData.email || 'N/A' }}
</v-card-title>
<v-text-field
v-else
v-model="editedData.email"
variant="outlined"
density="compact"
hide-details
type="email"
class="mt-1"
/>
</div>
</div>
</v-card>
</v-col>
<v-col cols="12" sm="6" class="mb-2">
<v-card variant="outlined" class="pa-4 h-100" color="surface-variant">
<div class="d-flex align-center">
<v-avatar
class="me-4"
size="40"
:style="{
background: 'rgba(var(--v-theme-info), 0.2)',
backdropFilter: 'blur(10px)',
webkitBackdropFilter: 'blur(10px)',
border: '1px solid rgba(var(--v-theme-info), 0.3)'
}"
>
<v-icon :icon="getFieldIcon('startDate')" color="info" />
</v-avatar>
<div class="flex-grow-1">
<v-card-subtitle class="pa-0 text-caption text-medium-emphasis">Start Date</v-card-subtitle>
<v-card-title v-if="!isEditMode" class="pa-0 text-h6 text-high-emphasis">
{{ formatDate(selectedRowData.startDate) }}
</v-card-title>
<v-text-field
v-else
v-model="editedData.startDate"
variant="outlined"
density="compact"
hide-details
type="date"
class="mt-1"
/>
</div>
</div>
</v-card>
</v-col>
<v-col cols="12" sm="6" class="mb-2">
<v-card variant="outlined" class="pa-4 h-100" color="surface-variant">
<div class="d-flex align-center">
<v-avatar
class="me-4"
size="40"
:style="{
background: 'rgba(var(--v-theme-success), 0.2)',
backdropFilter: 'blur(10px)',
webkitBackdropFilter: 'blur(10px)',
border: '1px solid rgba(var(--v-theme-success), 0.3)'
}"
>
<v-icon :icon="getFieldIcon('salary')" color="success" />
</v-avatar>
<div class="flex-grow-1">
<v-card-subtitle class="pa-0 text-caption text-medium-emphasis">Salary</v-card-subtitle>
<v-card-title v-if="!isEditMode" class="pa-0 text-h6 text-success font-weight-bold">
{{ formatSalary(selectedRowData.salary) }}
</v-card-title>
<v-text-field
v-else
v-model="editedData.salary"
variant="outlined"
density="compact"
hide-details
type="number"
prefix="$"
class="mt-1"
/>
</div>
</div>
</v-card>
</v-col>
<v-col cols="12" sm="6" class="mb-2">
<v-card variant="outlined" class="pa-4 h-100" color="surface-variant">
<div class="d-flex align-center">
<v-avatar
class="me-4"
size="40"
:style="{
background: 'rgba(var(--v-theme-warning), 0.2)',
backdropFilter: 'blur(10px)',
webkitBackdropFilter: 'blur(10px)',
border: '1px solid rgba(var(--v-theme-warning), 0.3)'
}"
>
<v-icon :icon="getFieldIcon('age')" color="warning" />
</v-avatar>
<div class="flex-grow-1">
<v-card-subtitle class="pa-0 text-caption text-medium-emphasis">Age</v-card-subtitle>
<v-card-title v-if="!isEditMode" class="pa-0 text-h6 text-high-emphasis">
{{ selectedRowData.age ? selectedRowData.age + ' years' : 'N/A' }}
</v-card-title>
<v-text-field
v-else
v-model="editedData.age"
variant="outlined"
density="compact"
hide-details
type="number"
suffix="years"
class="mt-1"
/>
</div>
</div>
</v-card>
</v-col>
<v-col cols="12" sm="6" class="mb-2">
<v-card variant="outlined" class="pa-4 h-100" color="surface-variant">
<div class="d-flex align-center">
<v-avatar
class="me-4"
size="40"
:style="{
background: `rgba(var(--v-theme-${getStatusColor(isEditMode ? editedData.status : selectedRowData.status)}), 0.2)`,
backdropFilter: 'blur(10px)',
webkitBackdropFilter: 'blur(10px)',
border: `1px solid rgba(var(--v-theme-${getStatusColor(isEditMode ? editedData.status : selectedRowData.status)}), 0.3)`
}"
>
<v-icon :icon="getFieldIcon('status')" :color="getStatusColor(isEditMode ? editedData.status : selectedRowData.status)" />
</v-avatar>
<div class="flex-grow-1">
<v-card-subtitle class="pa-0 text-caption text-medium-emphasis">Status</v-card-subtitle>
<div v-if="!isEditMode" class="pt-1">
<v-chip
:color="getStatusColor(selectedRowData.status)"
size="default"
variant="flat"
class="font-weight-medium"
>
<v-icon start icon="tabler-check" size="small" />
{{ getStatusLabel(selectedRowData.status) }}
</v-chip>
</div>
<v-select
v-else
v-model="editedData.status"
:items="statusOptions"
variant="outlined"
density="compact"
hide-details
class="mt-1"
>
<template #item="{ props, item }">
<v-list-item v-bind="props" :title="item.title">
<template #prepend>
<v-chip :color="item.raw.color" size="small" class="me-2">{{ item.title }}</v-chip>
</template>
</v-list-item>
</template>
<template #selection="{ item }">
<v-chip :color="item.raw.color" size="small">{{ item.title }}</v-chip>
</template>
</v-select>
</div>
</div>
</v-card>
</v-col>
</v-row>
<v-row class="mt-4">
<v-col cols="12">
<v-alert type="info" variant="tonal" class="mb-0" icon="tabler-info-circle">
<v-alert-title class="text-body-1 font-weight-medium">Summary Information</v-alert-title>
<div class="mt-2 text-body-2">
Employee
<strong>{{ (isEditMode ? editedData.fullName : selectedRowData.fullName) || 'Unknown' }}</strong>
{{ (isEditMode ? editedData.age : selectedRowData.age) ? `(${isEditMode ? editedData.age : selectedRowData.age} years old)` : '' }}
has been with the company since
<strong>{{ formatDate(isEditMode ? editedData.startDate : selectedRowData.startDate) }}</strong>
with a current status of
<strong>{{ getStatusLabel(isEditMode ? editedData.status : selectedRowData.status) }}</strong>.
</div>
</v-alert>
</v-col>
</v-row>
</v-container>
</v-form>
</v-card-text>
<v-divider />
</v-card>
</v-dialog>
</template>

View File

@@ -0,0 +1,210 @@
import { ref } from 'vue'
export function useDataTableActions(gridApi, rowData) {
const showDetailsDialog = ref(false)
const showDeleteDialog = ref(false)
const selectedRowData = ref(null)
const deleteRowData = ref(null)
const getRowDataByIndex = (rowIndex) => {
if (!gridApi.value) {
console.error('Grid API not available')
return null
}
try {
const node = gridApi.value.getDisplayedRowAtIndex(rowIndex)
if (node && node.data) {
return node.data
}
if (rowData.value && rowData.value[rowIndex]) {
return rowData.value[rowIndex]
}
console.error('Row data not found for index:', rowIndex)
return null
} catch (error) {
console.error('Error getting row data:', error)
return null
}
}
const showDetails = (rowIndex, nodeId) => {
console.log('Show details called with:', { rowIndex, nodeId })
const rowData = getRowDataByIndex(rowIndex)
if (rowData) {
selectedRowData.value = { ...rowData }
showDetailsDialog.value = true
console.log('Selected row data:', selectedRowData.value)
} else {
console.error('Failed to get row data for details')
}
}
const editRow = (rowIndex, nodeId) => {
console.log('Edit row called with:', { rowIndex, nodeId })
if (!gridApi.value) {
console.error('Grid API not available for editing')
return
}
try {
gridApi.value.startEditingCell({
rowIndex: rowIndex,
colKey: 'fullName'
})
console.log('Started inline editing for row:', rowIndex)
} catch (error) {
console.error('Error starting inline edit:', error)
}
}
const deleteRow = (rowIndex, nodeId) => {
console.log('Delete row called with:', { rowIndex, nodeId })
const rowData = getRowDataByIndex(rowIndex)
if (rowData) {
deleteRowData.value = { ...rowData }
showDeleteDialog.value = true
console.log('Delete row data:', deleteRowData.value)
} else {
console.error('Failed to get row data for delete')
}
}
const confirmDelete = () => {
if (deleteRowData.value && gridApi.value) {
try {
const indexToDelete = rowData.value.findIndex(item => {
return (
(item.id && item.id === deleteRowData.value.id) ||
(item.email && item.email === deleteRowData.value.email) ||
(item.fullName === deleteRowData.value.fullName &&
item.salary === deleteRowData.value.salary)
)
})
if (indexToDelete !== -1) {
rowData.value.splice(indexToDelete, 1)
gridApi.value.applyTransaction({
remove: [deleteRowData.value]
})
console.log('Row deleted successfully')
} else {
console.error('Row not found for deletion')
}
} catch (error) {
console.error('Error deleting row:', error)
}
}
showDeleteDialog.value = false
deleteRowData.value = null
}
const cancelDelete = () => {
showDeleteDialog.value = false
deleteRowData.value = null
}
const saveUser = (editedData) => {
if (!editedData || !gridApi.value) return
try {
const indexToUpdate = rowData.value.findIndex(item => {
return (
(item.id && item.id === editedData.id) ||
(item.email && item.email === selectedRowData.value.email) ||
(item.fullName === selectedRowData.value.fullName)
)
})
if (indexToUpdate !== -1) {
rowData.value[indexToUpdate] = { ...editedData }
const node = gridApi.value.getRowNode(indexToUpdate)
if (node) {
node.setData(editedData)
}
console.log('User updated successfully:', editedData)
} else {
console.error('User not found for update')
}
} catch (error) {
console.error('Error updating user:', error)
}
showDetailsDialog.value = false
selectedRowData.value = null
}
const exportToCSV = () => {
if (gridApi.value) {
gridApi.value.exportDataAsCsv({
fileName: `datatable-export-${new Date().toISOString().split('T')[0]}.csv`,
columnSeparator: ',',
suppressQuotes: false,
onlySelected: false,
skipGroups: true,
skipHeader: false,
skipFooters: true,
skipPinnedTop: true,
skipPinnedBottom: true,
allColumns: false,
onlySelectedAllPages: false
})
}
}
const exportToExcel = () => {
if (gridApi.value) {
gridApi.value.exportDataAsExcel({
fileName: `datatable-export-${new Date().toISOString().split('T')[0]}.xlsx`,
sheetName: 'Data Export',
onlySelected: false,
skipGroups: true,
skipHeader: false,
skipFooters: true,
skipPinnedTop: true,
skipPinnedBottom: true,
allColumns: false,
onlySelectedAllPages: false
})
}
}
const setupGlobalHandlers = () => {
window.handleShowDetails = showDetails
window.handleEditRow = editRow
window.handleDeleteRow = deleteRow
}
const cleanupGlobalHandlers = () => {
delete window.handleShowDetails
delete window.handleEditRow
delete window.handleDeleteRow
}
return {
showDetailsDialog,
showDeleteDialog,
selectedRowData,
deleteRowData,
showDetails,
editRow,
deleteRow,
confirmDelete,
cancelDelete,
saveUser,
exportToCSV,
exportToExcel,
setupGlobalHandlers,
cleanupGlobalHandlers
}
}

View File

@@ -0,0 +1,614 @@
import { ref, computed } from "vue";
export function useDataTableGrid(data, isMobile, isTablet) {
let gridApi = ref(null);
const rowData = ref([]);
const statusCellRenderer = (params) => {
const s = params.value;
let label = "Applied";
let colorClass = "info";
if (s === 1) {
label = "Current";
colorClass = "primary";
}
if (s === 2) {
label = "Professional";
colorClass = "success";
}
if (s === 3) {
label = "Rejected";
colorClass = "error";
}
if (s === 4) {
label = "Resigned";
colorClass = "warning";
}
const chipSize = isMobile.value ? "x-small" : "small";
const fontSize = isMobile.value ? "10px" : "12px";
return `<div class="v-chip v-chip--size-${chipSize} v-chip--variant-flat bg-${colorClass}"
style="
height:${isMobile.value ? "20px" : "24px"};
padding:0 ${isMobile.value ? "6px" : "8px"};
display:inline-flex;
align-items:center;
border-radius:12px;
font-size:${fontSize};
font-weight:500;
color:white;
border: inherit;
">${label}</div>`;
};
const statusCellEditor = () => {
const statusOptions = [
{ value: 0, label: "Applied" },
{ value: 1, label: "Current" },
{ value: 2, label: "Professional" },
{ value: 3, label: "Rejected" },
{ value: 4, label: "Resigned" },
];
let currentValue = 0;
let selectElement = null;
const editorObject = {
init: (params) => {
currentValue = params.value || 0;
selectElement = document.createElement("select");
selectElement.style.cssText =
"width: 100%; height: 100%; border: none; outline: none; font-size: 12px; background: white; color: black; padding: 4px;";
statusOptions.forEach((option) => {
const optionElement = document.createElement("option");
optionElement.value = option.value;
optionElement.text = option.label;
if (option.value == currentValue) {
optionElement.selected = true;
}
selectElement.appendChild(optionElement);
});
selectElement.addEventListener("change", (e) => {
currentValue = parseInt(e.target.value);
});
selectElement.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === "Tab") {
e.preventDefault();
currentValue = parseInt(selectElement.value);
params.stopEditing();
}
if (e.key === "Escape") {
params.stopEditing(true);
}
});
},
getGui: () => selectElement,
getValue: () => parseInt(currentValue),
afterGuiAttached: () => {
selectElement.focus();
selectElement.click();
},
isPopup: () => false,
};
return editorObject;
};
const actionButtonsRenderer = (params) => {
const iconSize = isMobile.value ? "14px" : "16px";
const buttonSize = isMobile.value ? "24px" : "28px";
const gap = isMobile.value ? "3px" : "4px";
const rowIndex = params.node.rowIndex;
const nodeId = params.node.id;
return `
<div style="display: flex; gap: ${gap}; align-items: center; justify-content: center; padding: 2px 0;">
<button
class="action-btn details-btn"
style="
background: rgba(var(--v-theme-primary), 0.15);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(var(--v-theme-primary), 0.3);
color: rgb(var(--v-theme-primary));
border-radius: 12px;
width: ${buttonSize};
height: ${buttonSize};
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 8px 32px rgba(var(--v-theme-primary), 0.12);
position: relative;
overflow: hidden;
"
onmouseover="
this.style.transform='translateY(-2px) scale(1.05)';
this.style.background='rgba(var(--v-theme-primary), 0.25)';
this.style.borderColor='rgba(var(--v-theme-primary), 0.5)';
this.style.boxShadow='0 12px 40px rgba(var(--v-theme-primary), 0.2)';
"
onmouseout="
this.style.transform='translateY(0) scale(1)';
this.style.background='rgba(var(--v-theme-primary), 0.15)';
this.style.borderColor='rgba(var(--v-theme-primary), 0.3)';
this.style.boxShadow='0 8px 32px rgba(var(--v-theme-primary), 0.12)';
"
onclick="handleShowDetails(${rowIndex}, '${nodeId}')"
title="View Details"
>
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</button>
<button
class="action-btn edit-btn"
style="
background: rgba(var(--v-theme-warning), 0.15);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(var(--v-theme-warning), 0.3);
color: rgb(var(--v-theme-warning));
border-radius: 12px;
width: ${buttonSize};
height: ${buttonSize};
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 8px 32px rgba(var(--v-theme-warning), 0.12);
position: relative;
overflow: hidden;
"
onmouseover="
this.style.transform='translateY(-2px) scale(1.05)';
this.style.background='rgba(var(--v-theme-warning), 0.25)';
this.style.borderColor='rgba(var(--v-theme-warning), 0.5)';
this.style.boxShadow='0 12px 40px rgba(var(--v-theme-warning), 0.2)';
"
onmouseout="
this.style.transform='translateY(0) scale(1)';
this.style.background='rgba(var(--v-theme-warning), 0.15)';
this.style.borderColor='rgba(var(--v-theme-warning), 0.3)';
this.style.boxShadow='0 8px 32px rgba(var(--v-theme-warning), 0.12)';
"
onclick="handleEditRow(${rowIndex}, '${nodeId}')"
title="Edit Row"
>
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="m18.5 2.5 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
</button>
<button
class="action-btn delete-btn"
style="
background: rgba(var(--v-theme-error), 0.15);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(var(--v-theme-error), 0.3);
color: rgb(var(--v-theme-error));
border-radius: 12px;
width: ${buttonSize};
height: ${buttonSize};
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 8px 32px rgba(var(--v-theme-error), 0.12);
position: relative;
overflow: hidden;
"
onmouseover="
this.style.transform='translateY(-2px) scale(1.05)';
this.style.background='rgba(var(--v-theme-error), 0.25)';
this.style.borderColor='rgba(var(--v-theme-error), 0.5)';
this.style.boxShadow='0 12px 40px rgba(var(--v-theme-error), 0.2)';
"
onmouseout="
this.style.transform='translateY(0) scale(1)';
this.style.background='rgba(var(--v-theme-error), 0.15)';
this.style.borderColor='rgba(var(--v-theme-error), 0.3)';
this.style.boxShadow='0 8px 32px rgba(var(--v-theme-error), 0.12)';
"
onclick="handleDeleteRow(${rowIndex}, '${nodeId}')"
title="Delete Row"
>
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
<path d="M3 6h18"></path>
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6m3 0V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
<line x1="10" y1="11" x2="10" y2="17"></line>
<line x1="14" y1="11" x2="14" y2="17"></line>
</svg>
</button>
</div>
`;
};
const columnDefs = ref([
{
headerName: "NAME",
field: "fullName",
sortable: true,
filter: true,
flex: 1,
minWidth: 150,
hide: false,
editable: true,
cellEditor: "agTextCellEditor",
cellEditorParams: {
maxLength: 50,
},
onCellValueChanged: (params) => {
console.log("Name changed:", params.newValue);
},
},
{
headerName: "EMAIL",
field: "email",
sortable: true,
filter: true,
flex: 1.5,
minWidth: 200,
hide: false,
editable: true,
cellEditor: "agTextCellEditor",
cellEditorParams: {
maxLength: 100,
},
onCellValueChanged: (params) => {
console.log("Email changed:", params.newValue);
},
},
{
headerName: "DATE",
field: "startDate",
sortable: true,
filter: true,
flex: 1,
minWidth: 120,
hide: false,
editable: true,
cellEditor: "agDateCellEditor",
cellEditorParams: {
preventEdit: (params) => {
return false;
},
},
valueFormatter: (params) => {
if (!params.value) return "";
const date = new Date(params.value);
if (isNaN(date.getTime())) return "";
const day = date.getDate().toString().padStart(2, "0");
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const year = date.getFullYear();
return `${day}/${month}/${year}`;
},
valueParser: (params) => {
if (!params.newValue) return null;
const dateStr = params.newValue;
if (typeof dateStr === "string" && dateStr.includes("/")) {
const parts = dateStr.split("/");
if (parts.length === 3) {
const day = parseInt(parts[0]);
const month = parseInt(parts[1]) - 1;
const year = parseInt(parts[2]);
const date = new Date(year, month, day);
if (!isNaN(date.getTime())) {
return date;
}
}
}
return new Date(dateStr);
},
filterParams: {
buttons: ["reset"],
closeOnApply: false,
suppressAndOrCondition: true,
comparator: (filterLocalDateAtMidnight, cellValue) => {
if (!cellValue) return -1;
const cellDate = new Date(cellValue);
cellDate.setHours(0, 0, 0, 0);
if (filterLocalDateAtMidnight.getTime() === cellDate.getTime()) {
return 0;
}
return filterLocalDateAtMidnight < cellDate ? -1 : 1;
},
},
onCellValueChanged: (params) => {
if (
params.newValue === "" ||
params.newValue === null ||
params.newValue === undefined
) {
params.node.setDataValue(params.colDef.field, params.oldValue);
}
console.log("Date changed:", params.newValue);
},
},
{
headerName: "SALARY",
field: "salary",
sortable: true,
filter: true,
flex: 1,
minWidth: 120,
hide: false,
editable: true,
cellEditor: "agNumberCellEditor",
cellEditorParams: {
precision: 0,
},
filterParams: {
buttons: ["reset"],
closeOnApply: false,
suppressAndOrCondition: true,
inRangeInclusive: true,
debounceMs: 200,
},
valueFormatter: (params) => {
if (params.value) {
const intValue = Math.floor(Number(params.value));
return `$${intValue.toLocaleString()}`;
}
return "";
},
onCellValueChanged: (params) => {
if (
params.newValue !== null &&
params.newValue !== undefined &&
params.newValue !== ""
) {
const numValue = parseInt(params.newValue);
if (!isNaN(numValue)) {
params.node.setDataValue(params.colDef.field, numValue);
} else {
params.node.setDataValue(params.colDef.field, params.oldValue);
}
} else {
params.node.setDataValue(params.colDef.field, params.oldValue);
}
console.log("Salary changed:", params.newValue);
},
},
{
headerName: "AGE",
field: "age",
sortable: true,
filter: true,
flex: 0.7,
minWidth: 80,
hide: false,
editable: true,
cellEditor: "agNumberCellEditor",
cellEditorParams: {
precision: 0,
showStepperButtons: false,
},
valueFormatter: (params) => {
if (
params.value === null ||
params.value === undefined ||
params.value === ""
) {
return "";
}
const numValue = Number(params.value);
return isNaN(numValue) ? "" : Math.floor(numValue).toString();
},
onCellValueChanged: (params) => {
const newValue = params.newValue;
if (newValue === null || newValue === undefined || newValue === "") {
params.node.setDataValue(params.colDef.field, params.oldValue);
return;
}
const cleanValue = String(newValue).trim();
if (cleanValue === "") {
params.node.setDataValue(params.colDef.field, params.oldValue);
return;
}
const parsed = parseInt(cleanValue, 10);
if (isNaN(parsed)) {
console.warn("Age: Invalid number format:", cleanValue);
params.node.setDataValue(params.colDef.field, params.oldValue);
return;
}
if (parsed < 1 || parsed > 120) {
console.warn("Age: Out of range (1-120):", parsed);
params.node.setDataValue(params.colDef.field, params.oldValue);
return;
}
params.node.setDataValue(params.colDef.field, parsed);
console.log("Age changed from", params.oldValue, "to", parsed);
},
},
{
headerName: "STATUS",
field: "status",
sortable: true,
filter: true,
flex: 1,
minWidth: 100,
hide: false,
editable: true,
cellRenderer: statusCellRenderer,
cellEditor: statusCellEditor,
onCellValueChanged: (params) => {
console.log("Status changed:", params.newValue);
},
},
{
headerName: "ACTION",
field: "action",
sortable: false,
filter: false,
flex: 1,
minWidth: 100,
hide: false,
editable: false,
cellRenderer: actionButtonsRenderer,
suppressMenu: true,
suppressSorting: true,
suppressFilter: true,
},
]);
const defaultColDef = computed(() => ({
resizable: true,
sortable: true,
filter: true,
floatingFilter: false,
cellStyle: { display: "flex", alignItems: "center" },
flex: 1,
minWidth: isMobile.value ? 80 : 100,
maxWidth: 400,
wrapText: false,
autoHeight: false,
suppressMenu: false,
}));
const gridOptions = computed(() => ({
theme: "legacy",
headerHeight: isMobile.value ? 48 : 56,
rowHeight: isMobile.value ? 44 : 52,
animateRows: true,
rowSelection: { type: "multiRow" },
pagination: true,
paginationPageSize: isMobile.value ? 5 : 10,
paginationPageSizeSelector: isMobile.value ? [5, 10, 20] : [5, 10, 20, 50],
suppressRowClickSelection: false,
rowMultiSelectWithClick: true,
enableCellTextSelection: true,
suppressHorizontalScroll: false,
alwaysShowHorizontalScroll: false,
suppressColumnVirtualisation: false,
autoGroupColumnDef: {
minWidth: 200,
maxWidth: 400,
flex: 1,
},
editType: "fullRow",
stopEditingWhenCellsLoseFocus: true,
enterNavigatesVertically: true,
enterNavigatesVerticallyAfterEdit: true,
singleClickEdit: false,
suppressClickEdit: false,
includeHiddenColumnsInQuickFilter: false,
cacheQuickFilter: true,
enableAdvancedFilter: false,
includeHiddenColumnsInAdvancedFilter: false,
suppressBrowserResizeObserver: false,
maintainColumnOrder: true,
suppressMenuHide: true,
enableRangeSelection: true,
enableFillHandle: false,
enableRangeHandle: false,
}));
const updateColumnVisibility = () => {
if (!gridApi.value) return;
if (isMobile.value) {
gridApi.value.setColumnVisible("email", false);
gridApi.value.setColumnVisible("startDate", false);
gridApi.value.setColumnVisible("age", false);
} else if (isTablet.value) {
gridApi.value.setColumnVisible("email", true);
gridApi.value.setColumnVisible("startDate", false);
gridApi.value.setColumnVisible("age", true);
} else {
gridApi.value.setColumnVisible("email", true);
gridApi.value.setColumnVisible("startDate", true);
gridApi.value.setColumnVisible("age", true);
}
};
const updatePagination = () => {
if (!gridApi.value) return;
const pageSize = isMobile.value ? 5 : 10;
const pageSizeSelector = isMobile.value ? [5, 10, 20] : [5, 10, 20, 50];
gridApi.value.paginationSetPageSize(pageSize);
gridApi.value.setGridOption("paginationPageSizeSelector", pageSizeSelector);
};
function onGridReady(params) {
gridApi.value = params.api;
rowData.value = data.map((item) => ({
...item,
salary:
typeof item.salary === "string" ? parseInt(item.salary) : item.salary,
age: typeof item.age === "string" ? parseInt(item.age) : item.age,
}));
updateColumnVisibility();
updatePagination();
setTimeout(() => {
gridApi.value.sizeColumnsToFit();
}, 100);
}
function onCellValueChanged(params) {
console.log("Cell value changed:", {
field: params.colDef.field,
oldValue: params.oldValue,
newValue: params.newValue,
data: params.data,
});
const dataIndex = rowData.value.findIndex((item) => {
return (
(item.id && item.id === params.data.id) ||
(item.email && item.email === params.data.email) ||
item.fullName === params.data.fullName
);
});
if (dataIndex !== -1) {
rowData.value[dataIndex] = { ...params.data };
}
}
return {
gridApi,
columnDefs,
rowData,
defaultColDef,
gridOptions,
onGridReady,
onCellValueChanged,
updateColumnVisibility,
updatePagination,
};
}

View File

@@ -0,0 +1,27 @@
import { ref, onMounted, onUnmounted } from 'vue'
export function useResponsive() {
const windowWidth = ref(window.innerWidth)
const isMobile = ref(window.innerWidth < 768)
const isTablet = ref(window.innerWidth >= 768 && window.innerWidth < 1024)
const handleResize = () => {
windowWidth.value = window.innerWidth
isMobile.value = window.innerWidth < 768
isTablet.value = window.innerWidth >= 768 && window.innerWidth < 1024
}
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
return {
windowWidth,
isMobile,
isTablet
}
}

View File

@@ -0,0 +1,447 @@
import avatar1 from "@images/avatars/avatar-1.png";
import avatar2 from "@images/avatars/avatar-2.png";
import avatar3 from "@images/avatars/avatar-3.png";
import avatar4 from "@images/avatars/avatar-4.png";
import avatar5 from "@images/avatars/avatar-5.png";
import avatar6 from "@images/avatars/avatar-6.png";
import avatar7 from "@images/avatars/avatar-7.png";
import avatar8 from "@images/avatars/avatar-8.png";
const data = [
{
responsiveId: "",
id: 95,
avatar: avatar2,
fullName: "Edwina Ebsworth",
post: "Human Resources Assistant",
email: "eebsworth2m@sbwire.com",
city: "Puzi",
startDate: "09/27/2018",
salary: 19586.23,
age: "27",
experience: "2 Years",
status: 1,
},
{
responsiveId: "",
id: 1,
avatar: avatar8,
fullName: "Korrie O'Crevy",
post: "Nuclear Power Engineer",
email: "kocrevy0@thetimes.co.uk",
city: "Krasnosilka",
startDate: "09/23/2016",
salary: 23896.35,
age: "61",
experience: "1 Year",
status: 2,
},
{
responsiveId: "",
id: 7,
avatar: "",
fullName: "Eileen Diehn",
post: "Environmental Specialist",
email: "ediehn6@163.com",
city: "Lampuyang",
startDate: "10/15/2017",
salary: 18991.67,
age: "59",
experience: "9 Years",
status: 3,
},
{
responsiveId: "",
id: 11,
avatar: "",
fullName: "De Falloon",
post: "Sales Representative",
email: "dfalloona@ifeng.com",
city: "Colima",
startDate: "06/12/2018",
salary: 19252.12,
age: "30",
experience: "0 Year",
status: 4,
},
{
responsiveId: "",
id: 3,
avatar: avatar7,
fullName: "Stella Ganderton",
post: "Operator",
email: "sganderton2@tuttocitta.it",
city: "Golcowa",
startDate: "03/24/2018",
salary: 13076.28,
age: "66",
experience: "6 Years",
status: 5,
},
{
responsiveId: "",
id: 5,
avatar: "",
fullName: "Harmonia Nisius",
post: "Senior Cost Accountant",
email: "hnisius4@gnu.org",
city: "Lucan",
startDate: "08/25/2017",
salary: 10909.52,
age: "33",
experience: "3 Years",
status: 2,
},
{
responsiveId: "",
id: 6,
avatar: "",
fullName: "Genevra Honeywood",
post: "Geologist",
email: "ghoneywood5@narod.ru",
city: "Maofan",
startDate: "06/01/2017",
salary: 17803.8,
age: "61",
experience: "1 Year",
status: 1,
},
{
responsiveId: "",
id: 4,
avatar: avatar8,
fullName: "Dorolice Crossman",
post: "Cost Accountant",
email: "dcrossman3@google.co.jp",
city: "Paquera",
startDate: "12/03/2017",
salary: 12336.17,
age: "22",
experience: "2 Years",
status: 2,
},
{
responsiveId: "",
id: 8,
avatar: avatar7,
fullName: "Richardo Aldren",
post: "Senior Sales Associate",
email: "raldren7@mtv.com",
city: "Skoghall",
startDate: "11/05/2016",
salary: 19230.13,
age: "55",
experience: "5 Years",
status: 3,
},
{
responsiveId: "",
id: 9,
avatar: avatar2,
fullName: "Allyson Moakler",
post: "Safety Technician",
email: "amoakler8@shareasale.com",
city: "Mogilany",
startDate: "12/29/2018",
salary: 11677.32,
age: "39",
experience: "9 Years",
status: 5,
},
{
responsiveId: "",
id: 10,
avatar: avatar7,
fullName: "Merline Penhalewick",
post: "Junior Executive",
email: "mpenhalewick9@php.net",
city: "Kanuma",
startDate: "04/19/2019",
salary: 15939.52,
age: "23",
experience: "3 Years",
status: 2,
},
{
responsiveId: "",
id: 12,
avatar: "",
fullName: "Cyrus Gornal",
post: "Senior Sales Associate",
email: "cgornalb@fda.gov",
city: "Boro Utara",
startDate: "12/09/2017",
salary: 16745.47,
age: "22",
experience: "2 Years",
status: 4,
},
{
responsiveId: "",
id: 13,
avatar: "",
fullName: "Tallou Balf",
post: "Staff Accountant",
email: "tbalfc@sina.com.cn",
city: "Siliana",
startDate: "01/21/2016",
salary: 15488.53,
age: "36",
experience: "6 Years",
status: 4,
},
{
responsiveId: "",
id: 14,
avatar: "",
fullName: "Othilia Extill",
post: "Associate Professor",
email: "oextilld@theatlantic.com",
city: "Brzyska",
startDate: "02/01/2016",
salary: 18442.34,
age: "43",
experience: "3 Years",
status: 2,
},
{
responsiveId: "",
id: 15,
avatar: "",
fullName: "Wilmar Bourton",
post: "Administrative Assistant",
email: "wbourtone@sakura.ne.jp",
city: "Bích Động",
startDate: "04/25/2018",
salary: 13304.45,
age: "19",
experience: "9 Years",
status: 5,
},
{
responsiveId: "",
id: 16,
avatar: avatar4,
fullName: "Robinson Brazenor",
post: "General Manager",
email: "rbrazenorf@symantec.com",
city: "Gendiwu",
startDate: "12/23/2017",
salary: 11953.08,
age: "66",
experience: "6 Years",
status: 5,
},
{
responsiveId: "",
id: 17,
avatar: "",
fullName: "Nadia Bettenson",
post: "Environmental Tech",
email: "nbettensong@joomla.org",
city: "Chabařovice",
startDate: "07/11/2018",
salary: 20484.44,
age: "64",
experience: "4 Years",
status: 1,
},
{
responsiveId: "",
id: 18,
avatar: "",
fullName: "Titus Hayne",
post: "Web Designer",
email: "thayneh@kickstarter.com",
city: "Yangon",
startDate: "05/25/2019",
salary: 16871.48,
age: "59",
experience: "9 Years",
status: 1,
},
{
responsiveId: "",
id: 19,
avatar: avatar4,
fullName: "Roxie Huck",
post: "Administrative Assistant",
email: "rhucki@ed.gov",
city: "Polýkastro",
startDate: "04/04/2019",
salary: 19653.56,
age: "41",
experience: "1 Year",
status: 4,
},
{
responsiveId: "",
id: 20,
avatar: avatar7,
fullName: "Latashia Lewtey",
post: "Actuary",
email: "llewteyj@sun.com",
city: "Hougong",
startDate: "08/03/2017",
salary: 18303.87,
age: "35",
experience: "5 Years",
status: 1,
},
{
responsiveId: "",
id: 21,
avatar: "",
fullName: "Natalina Tyne",
post: "Software Engineer",
email: "ntynek@merriam-webster.com",
city: "Yanguan",
startDate: "03/16/2019",
salary: 15256.4,
age: "30",
experience: "0 Year",
status: 2,
},
{
responsiveId: "",
id: 22,
avatar: "",
fullName: "Faun Josefsen",
post: "Analog Circuit Design manager",
email: "fjosefsenl@samsung.com",
city: "Wengyang",
startDate: "07/08/2017",
salary: 11209.16,
age: "40",
experience: "0 Year",
status: 3,
},
{
responsiveId: "",
id: 23,
avatar: avatar7,
fullName: "Rosmunda Steed",
post: "Assistant Media Planner",
email: "rsteedm@xing.com",
city: "Manzanares",
startDate: "12/23/2017",
salary: 13778.34,
age: "21",
experience: "1 Year",
status: 5,
},
{
responsiveId: "",
id: 24,
avatar: "",
fullName: "Scott Jiran",
post: "Graphic Designer",
email: "sjirann@simplemachines.org",
city: "Pinglin",
startDate: "05/26/2016",
salary: 23081.71,
age: "23",
experience: "3 Years",
status: 1,
},
{
responsiveId: "",
id: 25,
avatar: "",
fullName: "Carmita Medling",
post: "Accountant",
email: "cmedlingo@hp.com",
city: "Bourges",
startDate: "07/31/2019",
salary: 13602.24,
age: "47",
experience: "7 Years",
status: 3,
},
{
responsiveId: "",
id: 26,
avatar: avatar2,
fullName: "Morgen Benes",
post: "Senior Sales Associate",
email: "mbenesp@ted.com",
city: "Cà Mau",
startDate: "04/10/2016",
salary: 16969.63,
age: "42",
experience: "2 Years",
status: 4,
},
{
responsiveId: "",
id: 27,
avatar: "",
fullName: "Onfroi Doughton",
post: "Civil Engineer",
email: "odoughtonq@aboutads.info",
city: "Utrecht (stad)",
startDate: "09/29/2018",
salary: 23796.62,
age: "28",
experience: "8 Years",
status: 3,
},
{
responsiveId: "",
id: 28,
avatar: "",
fullName: "Kliment McGinney",
post: "Chief Design Engineer",
email: "kmcginneyr@paginegialle.it",
city: "Xiaocheng",
startDate: "07/09/2018",
salary: 24027.81,
age: "28",
experience: "8 Years",
status: 4,
},
{
responsiveId: "",
id: 29,
avatar: "",
fullName: "Devin Bridgland",
post: "Tax Accountant",
email: "dbridglands@odnoklassniki.ru",
city: "Baoli",
startDate: "07/17/2016",
salary: 13508.15,
age: "48",
experience: "8 Years",
status: 3,
},
{
responsiveId: "",
id: 30,
avatar: avatar6,
fullName: "Gilbert McFade",
post: "Biostatistician",
email: "gmcfadet@irs.gov",
city: "Deje",
startDate: "08/28/2018",
salary: 21632.3,
age: "20",
experience: "0 Year",
status: 2,
},
{
responsiveId: "",
id: 31,
avatar: "",
fullName: "Teressa Bleakman",
post: "Senior Editor",
email: "tbleakmanu@phpbb.com",
city: "Žebrák",
startDate: "09/03/2016",
salary: 24875.41,
age: "37",
experience: "7 Years",
status: 5,
},
];
export default data;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,86 @@
<script setup>
const desserts = [
{
dessert: 'Frozen Yogurt',
calories: 159,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Ice cream sandwich',
calories: 237,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Eclair',
calories: 262,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Cupcake',
calories: 305,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Gingerbread',
calories: 356,
fat: 6,
carbs: 24,
protein: 4,
},
]
</script>
<template>
<VTable class="text-no-wrap">
<thead>
<tr>
<th>
Desserts(100g Servings)
</th>
<th>
calories
</th>
<th>
Fat(g)
</th>
<th>
Carbs(g)
</th>
<th>
protein(g)
</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in desserts"
:key="item.dessert"
>
<td>
{{ item.dessert }}
</td>
<td>
{{ item.calories }}
</td>
<td>
{{ item.fat }}
</td>
<td>
{{ item.carbs }}
</td>
<td>
{{ item.protein }}
</td>
</tr>
</tbody>
</VTable>
</template>

View File

@@ -0,0 +1,89 @@
<script setup>
const desserts = [
{
dessert: 'Frozen Yogurt',
calories: 159,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Ice cream sandwich',
calories: 237,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Eclair',
calories: 262,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Cupcake',
calories: 305,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Gingerbread',
calories: 356,
fat: 6,
carbs: 24,
protein: 4,
},
]
</script>
<template>
<VTable
density="compact"
class="text-no-wrap"
>
<thead>
<tr>
<th>
Desserts(100g Servings)
</th>
<th>
calories
</th>
<th>
Fat(g)
</th>
<th>
Carbs(g)
</th>
<th>
protein(g)
</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in desserts"
:key="item.dessert"
>
<td>
{{ item.dessert }}
</td>
<td>
{{ item.calories }}
</td>
<td>
{{ item.fat }}
</td>
<td>
{{ item.carbs }}
</td>
<td>
{{ item.protein }}
</td>
</tr>
</tbody>
</VTable>
</template>

View File

@@ -0,0 +1,90 @@
<script setup>
const desserts = [
{
dessert: 'Frozen Yogurt',
calories: 159,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Ice cream sandwich',
calories: 237,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Eclair',
calories: 262,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Cupcake',
calories: 305,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Gingerbread',
calories: 356,
fat: 6,
carbs: 24,
protein: 4,
},
]
</script>
<template>
<VTable
height="250"
fixed-header
class="text-no-wrap"
>
<thead>
<tr>
<th>
Desserts(100g Servings)
</th>
<th>
calories
</th>
<th>
Fat(g)
</th>
<th>
Carbs(g)
</th>
<th>
protein(g)
</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in desserts"
:key="item.dessert"
>
<td>
{{ item.dessert }}
</td>
<td>
{{ item.calories }}
</td>
<td>
{{ item.fat }}
</td>
<td>
{{ item.carbs }}
</td>
<td>
{{ item.protein }}
</td>
</tr>
</tbody>
</VTable>
</template>

View File

@@ -0,0 +1,89 @@
<script setup>
const desserts = [
{
dessert: 'Frozen Yogurt',
calories: 159,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Ice cream sandwich',
calories: 237,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Eclair',
calories: 262,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Cupcake',
calories: 305,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Gingerbread',
calories: 356,
fat: 6,
carbs: 24,
protein: 4,
},
]
</script>
<template>
<VTable
height="250"
class="text-no-wrap"
>
<thead>
<tr>
<th>
Desserts(100g Servings)
</th>
<th>
calories
</th>
<th>
Fat(g)
</th>
<th>
Carbs(g)
</th>
<th>
protein(g)
</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in desserts"
:key="item.dessert"
>
<td>
{{ item.dessert }}
</td>
<td>
{{ item.calories }}
</td>
<td>
{{ item.fat }}
</td>
<td>
{{ item.carbs }}
</td>
<td>
{{ item.protein }}
</td>
</tr>
</tbody>
</VTable>
</template>

View File

@@ -0,0 +1,90 @@
<script setup>
const desserts = [
{
dessert: 'Frozen Yogurt',
calories: 159,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Ice cream sandwich',
calories: 237,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Eclair',
calories: 262,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Cupcake',
calories: 305,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Gingerbread',
calories: 356,
fat: 6,
carbs: 24,
protein: 4,
},
]
</script>
<template>
<VTable
theme="dark"
class="text-no-wrap rounded-0"
>
<thead>
<tr>
<th>
Desserts(100g Servings)
</th>
<th>
calories
</th>
<th>
Fat(g)
</th>
<th>
Carbs(g)
</th>
<th>
protein(g)
</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in desserts"
:key="item.dessert"
>
<td>
{{ item.dessert }}
</td>
<td>
{{ item.calories }}
</td>
<td>
{{ item.fat }}
</td>
<td>
{{ item.carbs }}
</td>
<td>
{{ item.protein }}
</td>
</tr>
</tbody>
</VTable>
</template>

View File

@@ -0,0 +1,898 @@
export const basic = { ts: `<script setup lang="ts">
const desserts = [
{
dessert: 'Frozen Yogurt',
calories: 159,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Ice cream sandwich',
calories: 237,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Eclair',
calories: 262,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Cupcake',
calories: 305,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Gingerbread',
calories: 356,
fat: 6,
carbs: 24,
protein: 4,
},
]
</script>
<template>
<VTable class="text-no-wrap">
<thead>
<tr>
<th>
Desserts(100g Servings)
</th>
<th>
calories
</th>
<th>
Fat(g)
</th>
<th>
Carbs(g)
</th>
<th>
protein(g)
</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in desserts"
:key="item.dessert"
>
<td>
{{ item.dessert }}
</td>
<td>
{{ item.calories }}
</td>
<td>
{{ item.fat }}
</td>
<td>
{{ item.carbs }}
</td>
<td>
{{ item.protein }}
</td>
</tr>
</tbody>
</VTable>
</template>
`, js: `<script setup>
const desserts = [
{
dessert: 'Frozen Yogurt',
calories: 159,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Ice cream sandwich',
calories: 237,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Eclair',
calories: 262,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Cupcake',
calories: 305,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Gingerbread',
calories: 356,
fat: 6,
carbs: 24,
protein: 4,
},
]
</script>
<template>
<VTable class="text-no-wrap">
<thead>
<tr>
<th>
Desserts(100g Servings)
</th>
<th>
calories
</th>
<th>
Fat(g)
</th>
<th>
Carbs(g)
</th>
<th>
protein(g)
</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in desserts"
:key="item.dessert"
>
<td>
{{ item.dessert }}
</td>
<td>
{{ item.calories }}
</td>
<td>
{{ item.fat }}
</td>
<td>
{{ item.carbs }}
</td>
<td>
{{ item.protein }}
</td>
</tr>
</tbody>
</VTable>
</template>
` }
export const density = { ts: `<script setup lang="ts">
const desserts = [
{
dessert: 'Frozen Yogurt',
calories: 159,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Ice cream sandwich',
calories: 237,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Eclair',
calories: 262,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Cupcake',
calories: 305,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Gingerbread',
calories: 356,
fat: 6,
carbs: 24,
protein: 4,
},
]
</script>
<template>
<VTable
density="compact"
class="text-no-wrap"
>
<thead>
<tr>
<th>
Desserts(100g Servings)
</th>
<th>
calories
</th>
<th>
Fat(g)
</th>
<th>
Carbs(g)
</th>
<th>
protein(g)
</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in desserts"
:key="item.dessert"
>
<td>
{{ item.dessert }}
</td>
<td>
{{ item.calories }}
</td>
<td>
{{ item.fat }}
</td>
<td>
{{ item.carbs }}
</td>
<td>
{{ item.protein }}
</td>
</tr>
</tbody>
</VTable>
</template>
`, js: `<script setup>
const desserts = [
{
dessert: 'Frozen Yogurt',
calories: 159,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Ice cream sandwich',
calories: 237,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Eclair',
calories: 262,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Cupcake',
calories: 305,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Gingerbread',
calories: 356,
fat: 6,
carbs: 24,
protein: 4,
},
]
</script>
<template>
<VTable
density="compact"
class="text-no-wrap"
>
<thead>
<tr>
<th>
Desserts(100g Servings)
</th>
<th>
calories
</th>
<th>
Fat(g)
</th>
<th>
Carbs(g)
</th>
<th>
protein(g)
</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in desserts"
:key="item.dessert"
>
<td>
{{ item.dessert }}
</td>
<td>
{{ item.calories }}
</td>
<td>
{{ item.fat }}
</td>
<td>
{{ item.carbs }}
</td>
<td>
{{ item.protein }}
</td>
</tr>
</tbody>
</VTable>
</template>
` }
export const fixedHeader = { ts: `<script setup lang="ts">
const desserts = [
{
dessert: 'Frozen Yogurt',
calories: 159,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Ice cream sandwich',
calories: 237,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Eclair',
calories: 262,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Cupcake',
calories: 305,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Gingerbread',
calories: 356,
fat: 6,
carbs: 24,
protein: 4,
},
]
</script>
<template>
<VTable
height="250"
fixed-header
class="text-no-wrap"
>
<thead>
<tr>
<th>
Desserts(100g Servings)
</th>
<th>
calories
</th>
<th>
Fat(g)
</th>
<th>
Carbs(g)
</th>
<th>
protein(g)
</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in desserts"
:key="item.dessert"
>
<td>
{{ item.dessert }}
</td>
<td>
{{ item.calories }}
</td>
<td>
{{ item.fat }}
</td>
<td>
{{ item.carbs }}
</td>
<td>
{{ item.protein }}
</td>
</tr>
</tbody>
</VTable>
</template>
`, js: `<script setup>
const desserts = [
{
dessert: 'Frozen Yogurt',
calories: 159,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Ice cream sandwich',
calories: 237,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Eclair',
calories: 262,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Cupcake',
calories: 305,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Gingerbread',
calories: 356,
fat: 6,
carbs: 24,
protein: 4,
},
]
</script>
<template>
<VTable
height="250"
fixed-header
class="text-no-wrap"
>
<thead>
<tr>
<th>
Desserts(100g Servings)
</th>
<th>
calories
</th>
<th>
Fat(g)
</th>
<th>
Carbs(g)
</th>
<th>
protein(g)
</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in desserts"
:key="item.dessert"
>
<td>
{{ item.dessert }}
</td>
<td>
{{ item.calories }}
</td>
<td>
{{ item.fat }}
</td>
<td>
{{ item.carbs }}
</td>
<td>
{{ item.protein }}
</td>
</tr>
</tbody>
</VTable>
</template>
` }
export const height = { ts: `<script setup lang="ts">
const desserts = [
{
dessert: 'Frozen Yogurt',
calories: 159,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Ice cream sandwich',
calories: 237,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Eclair',
calories: 262,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Cupcake',
calories: 305,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Gingerbread',
calories: 356,
fat: 6,
carbs: 24,
protein: 4,
},
]
</script>
<template>
<VTable
height="250"
class="text-no-wrap"
>
<thead>
<tr>
<th>
Desserts(100g Servings)
</th>
<th>
calories
</th>
<th>
Fat(g)
</th>
<th>
Carbs(g)
</th>
<th>
protein(g)
</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in desserts"
:key="item.dessert"
>
<td>
{{ item.dessert }}
</td>
<td>
{{ item.calories }}
</td>
<td>
{{ item.fat }}
</td>
<td>
{{ item.carbs }}
</td>
<td>
{{ item.protein }}
</td>
</tr>
</tbody>
</VTable>
</template>
`, js: `<script setup>
const desserts = [
{
dessert: 'Frozen Yogurt',
calories: 159,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Ice cream sandwich',
calories: 237,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Eclair',
calories: 262,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Cupcake',
calories: 305,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Gingerbread',
calories: 356,
fat: 6,
carbs: 24,
protein: 4,
},
]
</script>
<template>
<VTable
height="250"
class="text-no-wrap"
>
<thead>
<tr>
<th>
Desserts(100g Servings)
</th>
<th>
calories
</th>
<th>
Fat(g)
</th>
<th>
Carbs(g)
</th>
<th>
protein(g)
</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in desserts"
:key="item.dessert"
>
<td>
{{ item.dessert }}
</td>
<td>
{{ item.calories }}
</td>
<td>
{{ item.fat }}
</td>
<td>
{{ item.carbs }}
</td>
<td>
{{ item.protein }}
</td>
</tr>
</tbody>
</VTable>
</template>
` }
export const theme = { ts: `<script setup lang="ts">
const desserts = [
{
dessert: 'Frozen Yogurt',
calories: 159,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Ice cream sandwich',
calories: 237,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Eclair',
calories: 262,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Cupcake',
calories: 305,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Gingerbread',
calories: 356,
fat: 6,
carbs: 24,
protein: 4,
},
]
</script>
<template>
<VTable
theme="dark"
class="text-no-wrap rounded-0"
>
<thead>
<tr>
<th>
Desserts(100g Servings)
</th>
<th>
calories
</th>
<th>
Fat(g)
</th>
<th>
Carbs(g)
</th>
<th>
protein(g)
</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in desserts"
:key="item.dessert"
>
<td>
{{ item.dessert }}
</td>
<td>
{{ item.calories }}
</td>
<td>
{{ item.fat }}
</td>
<td>
{{ item.carbs }}
</td>
<td>
{{ item.protein }}
</td>
</tr>
</tbody>
</VTable>
</template>
`, js: `<script setup>
const desserts = [
{
dessert: 'Frozen Yogurt',
calories: 159,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Ice cream sandwich',
calories: 237,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Eclair',
calories: 262,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Cupcake',
calories: 305,
fat: 6,
carbs: 24,
protein: 4,
},
{
dessert: 'Gingerbread',
calories: 356,
fat: 6,
carbs: 24,
protein: 4,
},
]
</script>
<template>
<VTable
theme="dark"
class="text-no-wrap rounded-0"
>
<thead>
<tr>
<th>
Desserts(100g Servings)
</th>
<th>
calories
</th>
<th>
Fat(g)
</th>
<th>
Carbs(g)
</th>
<th>
protein(g)
</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in desserts"
:key="item.dessert"
>
<td>
{{ item.dessert }}
</td>
<td>
{{ item.calories }}
</td>
<td>
{{ item.fat }}
</td>
<td>
{{ item.carbs }}
</td>
<td>
{{ item.protein }}
</td>
</tr>
</tbody>
</VTable>
</template>
` }