refactor: replace common confirm dialogs with maintenance CRUD dialogs and streamline form handling in MasterDetailMntC.vue and SingleRecordMnt.vue
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
<template>
|
||||
<div class="d-flex flex-column">
|
||||
<v-card variant="flat">
|
||||
<v-card-title class="d-flex flex-wrap align-center py-0 ga-2">
|
||||
<span class="text-h6">可編輯表格維護示範</span>
|
||||
<v-chip :color="hasAnyChange ? 'warning' : 'success'" variant="tonal">
|
||||
{{ hasAnyChange ? '有未儲存變更' : '已同步' }}
|
||||
</v-chip>
|
||||
<v-chip color="info" variant="tonal">筆數 {{ filteredStudents.length }}</v-chip>
|
||||
<v-spacer />
|
||||
<v-btn flat :prepend-icon="mdiMagnify" @click="isSearchVisible = !isSearchVisible">條件搜尋</v-btn>
|
||||
<v-switch v-model="isBulkEditEnabled" color="primary" hide-details label="啟用編輯" />
|
||||
</v-card-title>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-text class="pb-0 pt-2">
|
||||
<v-row v-if="isSearchVisible" class="mb-2" dense>
|
||||
<v-col cols="12" md="3">
|
||||
<div class="text-body-1 text-medium-emphasis pl-2">學號</div>
|
||||
<v-text-field
|
||||
v-model="search.studentId"
|
||||
clearable
|
||||
density="compact"
|
||||
hide-details
|
||||
placeholder="例如:S2024001"
|
||||
variant="outlined"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<div class="text-body-1 text-medium-emphasis pl-2">姓名</div>
|
||||
<v-text-field
|
||||
v-model="search.name"
|
||||
clearable
|
||||
density="compact"
|
||||
hide-details
|
||||
placeholder="例如:王小明"
|
||||
variant="outlined"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<div class="text-body-1 text-medium-emphasis pl-2">系所</div>
|
||||
<v-select
|
||||
v-model="search.department"
|
||||
:class="{ 'select-hide-arrow': !isBulkEditEnabled }"
|
||||
clearable
|
||||
density="compact"
|
||||
hide-details
|
||||
:items="departments"
|
||||
variant="outlined"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="isBulkEditEnabled" align="center" class="mb-2 ga-1" dense>
|
||||
<v-btn
|
||||
:disabled="!hasSelectedRows"
|
||||
:prepend-icon="mdiDelete"
|
||||
variant="outlined"
|
||||
@click="deleteSelectedRows"
|
||||
>
|
||||
批次刪除
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
:disabled="!hasAnyChange"
|
||||
:prepend-icon="mdiContentSave"
|
||||
variant="outlined"
|
||||
@click="saveAllRows"
|
||||
>
|
||||
儲存變更
|
||||
</v-btn>
|
||||
<v-btn
|
||||
:disabled="!hasAnyChange"
|
||||
:prepend-icon="mdiRestore"
|
||||
variant="text"
|
||||
@click="resetAllRows"
|
||||
>
|
||||
取消變更
|
||||
</v-btn>
|
||||
</v-row>
|
||||
|
||||
<div ref="tableContainerRef">
|
||||
<v-data-table
|
||||
density="comfortable"
|
||||
fixed-header
|
||||
:headers="tableHeaders"
|
||||
:height="tableHeight"
|
||||
item-value="id"
|
||||
:items="filteredStudents"
|
||||
:items-per-page="10"
|
||||
items-per-page-text="每頁筆數"
|
||||
page-text="第 {0}-{1} 筆 / 共 {2} 筆"
|
||||
>
|
||||
<template #[`header.select`]>
|
||||
<v-checkbox-btn
|
||||
:disabled="!isBulkEditEnabled"
|
||||
:indeterminate="isPartiallyVisibleSelected"
|
||||
:model-value="isAllVisibleSelected"
|
||||
@update:model-value="toggleSelectAll"
|
||||
/>
|
||||
</template>
|
||||
<template #[`item.select`]="{ item }">
|
||||
<v-checkbox-btn
|
||||
:disabled="!isBulkEditEnabled"
|
||||
:model-value="selectedRowIds.includes(item.id)"
|
||||
@update:model-value="(checked) => toggleSingleRowSelection(item.id, checked)"
|
||||
/>
|
||||
</template>
|
||||
<template #[`item.studentId`]="{ item }">
|
||||
<v-text-field
|
||||
:model-value="getDraftRow(item.id)?.studentId ?? ''"
|
||||
density="compact"
|
||||
flat
|
||||
hide-details
|
||||
:readonly="!isBulkEditEnabled"
|
||||
:variant="isBulkEditEnabled ? 'outlined' : 'solo'"
|
||||
@update:model-value="
|
||||
(value) => {
|
||||
const row = getDraftRow(item.id)
|
||||
if (row) row.studentId = String(value)
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template #[`item.name`]="{ item }">
|
||||
<v-text-field
|
||||
:model-value="getDraftRow(item.id)?.name ?? ''"
|
||||
density="compact"
|
||||
flat
|
||||
hide-details
|
||||
:readonly="!isBulkEditEnabled"
|
||||
:variant="isBulkEditEnabled ? 'outlined' : 'solo'"
|
||||
@update:model-value="
|
||||
(value) => {
|
||||
const row = getDraftRow(item.id)
|
||||
if (row) row.name = String(value)
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template #[`item.department`]="{ item }">
|
||||
<v-select
|
||||
:model-value="getDraftRow(item.id)?.department ?? ''"
|
||||
:class="{ 'select-hide-arrow': !isBulkEditEnabled }"
|
||||
density="compact"
|
||||
flat
|
||||
hide-details
|
||||
:items="departments"
|
||||
:readonly="!isBulkEditEnabled"
|
||||
:variant="isBulkEditEnabled ? 'outlined' : 'solo'"
|
||||
@update:model-value="
|
||||
(value) => {
|
||||
const row = getDraftRow(item.id)
|
||||
if (row) row.department = String(value)
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template #[`item.grade`]="{ item }">
|
||||
<v-select
|
||||
:model-value="getDraftRow(item.id)?.grade ?? null"
|
||||
:class="{ 'select-hide-arrow': !isBulkEditEnabled }"
|
||||
density="compact"
|
||||
flat
|
||||
hide-details
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
:items="gradeOptions"
|
||||
:readonly="!isBulkEditEnabled"
|
||||
:variant="isBulkEditEnabled ? 'outlined' : 'solo'"
|
||||
@update:model-value="
|
||||
(value) => {
|
||||
const row = getDraftRow(item.id)
|
||||
if (row) row.grade = Number(value) || 0
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template #[`item.enrollYear`]="{ item }">
|
||||
<v-select
|
||||
:model-value="getDraftRow(item.id)?.enrollYear ?? null"
|
||||
:class="{ 'select-hide-arrow': !isBulkEditEnabled }"
|
||||
density="compact"
|
||||
flat
|
||||
hide-details
|
||||
:items="enrollYears"
|
||||
:readonly="!isBulkEditEnabled"
|
||||
:variant="isBulkEditEnabled ? 'outlined' : 'solo'"
|
||||
@update:model-value="
|
||||
(value) => {
|
||||
const row = getDraftRow(item.id)
|
||||
if (row) row.enrollYear = Number(value) || 0
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template #[`item.credits`]="{ item }">
|
||||
<v-text-field
|
||||
:model-value="getDraftRow(item.id)?.credits ?? 0"
|
||||
density="compact"
|
||||
flat
|
||||
hide-details
|
||||
min="0"
|
||||
:readonly="!isBulkEditEnabled"
|
||||
type="number"
|
||||
:variant="isBulkEditEnabled ? 'outlined' : 'solo'"
|
||||
@update:model-value="
|
||||
(value) => {
|
||||
const row = getDraftRow(item.id)
|
||||
if (row) row.credits = Number(value) || 0
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template #[`item.advisor`]="{ item }">
|
||||
<v-text-field
|
||||
:model-value="getDraftRow(item.id)?.advisor ?? ''"
|
||||
density="compact"
|
||||
flat
|
||||
hide-details
|
||||
:readonly="!isBulkEditEnabled"
|
||||
:variant="isBulkEditEnabled ? 'outlined' : 'solo'"
|
||||
@update:model-value="
|
||||
(value) => {
|
||||
const row = getDraftRow(item.id)
|
||||
if (row) row.advisor = String(value)
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template #[`item.email`]="{ item }">
|
||||
<v-text-field
|
||||
:model-value="getDraftRow(item.id)?.email ?? ''"
|
||||
density="compact"
|
||||
flat
|
||||
hide-details
|
||||
:readonly="!isBulkEditEnabled"
|
||||
:variant="isBulkEditEnabled ? 'outlined' : 'solo'"
|
||||
@update:model-value="
|
||||
(value) => {
|
||||
const row = getDraftRow(item.id)
|
||||
if (row) row.email = String(value)
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template #[`item.phone`]="{ item }">
|
||||
<v-text-field
|
||||
:model-value="getDraftRow(item.id)?.phone ?? ''"
|
||||
density="compact"
|
||||
flat
|
||||
hide-details
|
||||
:readonly="!isBulkEditEnabled"
|
||||
:variant="isBulkEditEnabled ? 'outlined' : 'solo'"
|
||||
@update:model-value="
|
||||
(value) => {
|
||||
const row = getDraftRow(item.id)
|
||||
if (row) row.phone = String(value)
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template #[`item.status`]="{ item }">
|
||||
<v-select
|
||||
:model-value="getDraftRow(item.id)?.status ?? ''"
|
||||
:class="{ 'select-hide-arrow': !isBulkEditEnabled }"
|
||||
density="compact"
|
||||
flat
|
||||
hide-details
|
||||
:items="statuses"
|
||||
:readonly="!isBulkEditEnabled"
|
||||
:variant="isBulkEditEnabled ? 'outlined' : 'solo'"
|
||||
@update:model-value="
|
||||
(value) => {
|
||||
const row = getDraftRow(item.id)
|
||||
if (row) row.status = String(value)
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template #[`item.actions`]="{ item }">
|
||||
<div class="d-flex ga-1 justify-end">
|
||||
<v-btn
|
||||
color="error"
|
||||
:disabled="!isBulkEditEnabled"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="deleteSingleRow(item.id)"
|
||||
>
|
||||
刪除
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { mdiContentSave, mdiDelete, mdiMagnify, mdiRestore } from '@mdi/js'
|
||||
import { useEditableStudentGrid } from '@/composables/maintenance/useEditableStudentGrid'
|
||||
|
||||
const {
|
||||
departments,
|
||||
draftRows,
|
||||
enrollYears,
|
||||
filteredStudents,
|
||||
getDraftRow,
|
||||
gradeOptions,
|
||||
hasAnyChange,
|
||||
hasSelectedRows,
|
||||
isAllVisibleSelected,
|
||||
isBulkEditEnabled,
|
||||
isPartiallyVisibleSelected,
|
||||
isSearchVisible,
|
||||
saveAllRows,
|
||||
search,
|
||||
selectedRowIds,
|
||||
statuses,
|
||||
tableContainerRef,
|
||||
tableHeaders,
|
||||
tableHeight,
|
||||
toggleSelectAll,
|
||||
toggleSingleRowSelection,
|
||||
deleteSelectedRows,
|
||||
deleteSingleRow,
|
||||
resetAllRows,
|
||||
} = useEditableStudentGrid()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.select-hide-arrow .v-field__append-inner) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.v-table__wrapper .v-field__input) {
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
:deep(.v-data-table-footer) {
|
||||
padding: 4px 0 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user