feat: reusable ag grid

This commit is contained in:
2025-09-03 17:28:04 +03:30
parent dafb07375a
commit c382d97d8d
7 changed files with 1311 additions and 560 deletions

View File

@@ -15,7 +15,7 @@
<v-card-text class="py-6">
<div class="text-body-1 mb-4">
Are you sure you want to delete this user?
Are you sure you want to delete this record?
</div>
<v-card
@@ -24,15 +24,14 @@
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
v-for="(val, key) in previewFields"
:key="key"
class="d-flex align-center"
>
<VIcon icon="tabler-dot" size="small" class="me-2" />
<strong class="me-2">{{ key }}:</strong>
{{ formatValue(val) }}
</div>
</div>
</v-card>
@@ -85,7 +84,7 @@
</template>
<script setup>
defineProps({
const props = defineProps({
modelValue: {
type: Boolean,
default: false
@@ -97,6 +96,10 @@ defineProps({
isMobile: {
type: Boolean,
default: false
},
columns: {
type: [Array, Object],
default: () => []
}
})
@@ -118,6 +121,26 @@ const handleConfirm = () => {
emit('update:model-value', false)
emit('confirm')
}
const previewFields = computed(() => {
const cols = Array.isArray(props.columns) ? props.columns : (props.columns?.value || [])
const prefer = ['title','name','fullName','todo','email','id','userId']
const keys = []
for (const p of prefer) {
if (props.selectedRowData && props.selectedRowData[p] !== undefined) keys.push(p)
}
for (const c of cols) {
if (c.field && !keys.includes(c.field) && c.field !== 'action' && keys.length < 4) keys.push(c.field)
}
const map = {}
keys.forEach(k => { map[k] = props.selectedRowData?.[k] })
return map
})
const formatValue = (v) => {
if (Array.isArray(v) || typeof v === 'object') return JSON.stringify(v)
return v ?? 'N/A'
}
</script>
<style scoped>

View File

@@ -2,7 +2,11 @@
const props = defineProps({
modelValue: Boolean,
selectedRowData: Object,
isMobile: Boolean
isMobile: Boolean,
columns: {
type: [Array, Object],
default: () => []
}
})
const emit = defineEmits(['update:modelValue', 'save-user'])
@@ -111,6 +115,11 @@ const getFieldIcon = (field) => {
}
return iconMap[field] || 'tabler-info-circle'
}
const visibleColumns = computed(() => {
const list = Array.isArray(props.columns) ? props.columns : (props.columns?.value || [])
return list.filter(c => c.field && c.field !== 'action')
})
</script>
<template>
@@ -179,7 +188,11 @@ const getFieldIcon = (field) => {
<v-form @keyup.enter="handleEnterKey">
<v-container fluid class="pa-0">
<v-row>
<v-col cols="12" sm="6" class="mb-2">
<v-col
v-for="col in visibleColumns"
:key="col.field"
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
@@ -192,212 +205,35 @@ const getFieldIcon = (field) => {
border: '1px solid rgba(var(--v-theme-primary), 0.3)'
}"
>
<v-icon :icon="getFieldIcon('name')" color="primary" />
<v-icon :icon="getFieldIcon(col.field)" 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>
<v-card-subtitle class="pa-0 text-caption text-medium-emphasis">{{ col.headerName || col.field }}</v-card-subtitle>
<template v-if="!isEditMode">
<v-card-title class="pa-0 text-body-1 text-high-emphasis">
{{ Array.isArray(selectedRowData[col.field]) || typeof selectedRowData[col.field] === 'object' ? JSON.stringify(selectedRowData[col.field]) : (selectedRowData[col.field] ?? 'N/A') }}
</v-card-title>
</template>
<template v-else>
<v-text-field
v-if="col.editable !== false && col.field !== 'status'"
v-model="editedData[col.field]"
variant="outlined"
density="compact"
hide-details
class="mt-1"
:type="col.field.toLowerCase().includes('date') ? 'date' : (typeof selectedRowData[col.field] === 'number' ? 'number' : 'text')"
/>
<v-select
v-else-if="col.field === 'status'"
v-model="editedData.status"
:items="statusOptions"
variant="outlined"
density="compact"
hide-details
class="mt-1"
/>
</template>
</div>
</div>
</v-card>