422 lines
14 KiB
Vue
422 lines
14 KiB
Vue
<script setup lang="ts">
|
||
import BaseFormSelect from '@/components/base/BaseFormSelect.vue'
|
||
import BaseFormTextField from '@/components/base/BaseFormTextField.vue'
|
||
import ConfirmDialog from '@/components/maint/CommonConfirmDialog.vue'
|
||
import DetailNavigation from '@/components/maint/master-detail/DetailNavigation.vue'
|
||
import DetailSidePanel from '@/components/maint/master-detail/DetailSidePanel.vue'
|
||
import MasterFileFormFields from '@/components/maint/MasterFileFormFields.vue'
|
||
import MntDialogCard from '@/components/maint/MntDialogCard.vue'
|
||
import MntRecordNavToolbar from '@/components/maint/MntRecordNavToolbar.vue'
|
||
import MaintShell from '@/components/maint/MaintShell.vue'
|
||
import SectionDataTable from '@/components/sections/SectionDataTable.vue'
|
||
import { useMasterDetailAMaintenancePage } from '@/composables/page-drivers/useMasterDetailAMaintenancePage'
|
||
|
||
const {
|
||
confirmSave, currentPage, departments, detailForm, flow, formState,
|
||
gradeOptions, itemsPerPage, masterDetailEvents, masterDetailProps,
|
||
openAddDialog, openEditDialog, openViewDialog, pageCount, pageModel,
|
||
pageSummary, requestSaveConfirmation, resetSearch, scrollToField,
|
||
search, searchPanelOpen, snackbarVisible, statuses, students, tableHeaders,
|
||
} = useMasterDetailAMaintenancePage()
|
||
</script>
|
||
|
||
<template>
|
||
<MaintShell
|
||
:search-panel-open="searchPanelOpen"
|
||
:title="pageModel.title"
|
||
@toggle-search="searchPanelOpen = !searchPanelOpen"
|
||
@create="openAddDialog"
|
||
>
|
||
<template #search-fields>
|
||
<v-col cols="12" md="2">
|
||
<BaseFormTextField
|
||
id="search-student-id"
|
||
v-model="search.studentId"
|
||
label="學號"
|
||
:label-char-count="2"
|
||
name="searchStudentId"
|
||
placeholder="例如:S2024001"
|
||
/>
|
||
</v-col>
|
||
<v-col cols="12" md="2">
|
||
<BaseFormTextField
|
||
id="search-name"
|
||
v-model="search.name"
|
||
label="姓名"
|
||
:label-char-count="2"
|
||
name="searchName"
|
||
placeholder="例如:王小明"
|
||
/>
|
||
</v-col>
|
||
<v-col cols="12" md="2">
|
||
<BaseFormSelect
|
||
id="search-department"
|
||
v-model="search.department"
|
||
label="系所"
|
||
:label-char-count="2"
|
||
:items="departments"
|
||
name="searchDepartment"
|
||
/>
|
||
</v-col>
|
||
<v-col cols="12" md="2">
|
||
<BaseFormSelect
|
||
id="search-grade"
|
||
v-model="search.grade"
|
||
label="年級"
|
||
:label-char-count="2"
|
||
item-title="title"
|
||
item-value="value"
|
||
:items="gradeOptions"
|
||
name="searchGrade"
|
||
/>
|
||
</v-col>
|
||
<v-col cols="12" md="2">
|
||
<BaseFormSelect
|
||
id="search-status"
|
||
v-model="search.status"
|
||
label="狀態"
|
||
:label-char-count="2"
|
||
:items="statuses"
|
||
name="searchStatus"
|
||
/>
|
||
</v-col>
|
||
<v-col class="d-flex justify-end align-end flex-grow-1 ga-2" cols="12" md="auto">
|
||
<v-btn variant="text" @click="resetSearch">清除</v-btn>
|
||
<v-btn color="primary" disabled variant="tonal">查詢</v-btn>
|
||
</v-col>
|
||
</template>
|
||
<template #table>
|
||
<SectionDataTable
|
||
:current-page="currentPage"
|
||
:grade-label="formState.gradeLabel"
|
||
:headers="tableHeaders"
|
||
:items="students"
|
||
:items-per-page="itemsPerPage"
|
||
:page-count="pageCount"
|
||
:page-summary="pageSummary"
|
||
:row-props="formState.rowProps"
|
||
:status-color="formState.statusColor"
|
||
@delete="flow.requestDeleteConfirmation($event)"
|
||
@edit="openEditDialog($event)"
|
||
@update:current-page="currentPage = $event"
|
||
@view="openViewDialog($event)"
|
||
/>
|
||
</template>
|
||
</MaintShell>
|
||
|
||
<teleport to="body">
|
||
<v-overlay
|
||
class="dialog-overlay"
|
||
:close-on-content-click="false"
|
||
:model-value="masterDetailProps.dialogVisible"
|
||
scrim="rgba(0, 0, 0, 0.45)"
|
||
scroll-strategy="block"
|
||
@update:model-value="flow.handleDialogVisibility($event)"
|
||
>
|
||
<div class="dialog-panel" :class="{ 'is-mobile': masterDetailProps.isMobile }">
|
||
<div
|
||
v-if="!masterDetailProps.isMobile || masterDetailProps.activeMobilePanel === 'detail'"
|
||
class="detail-panel-wrapper"
|
||
:class="{ 'is-active': !!masterDetailProps.selectedSemesterId, 'is-mobile': masterDetailProps.isMobile }"
|
||
>
|
||
<DetailSidePanel
|
||
v-model:detail-form="detailForm"
|
||
:is-detail-editing="masterDetailProps.isDetailEditing"
|
||
:is-mobile="masterDetailProps.isMobile"
|
||
:is-view-mode="masterDetailProps.isViewMode"
|
||
:selected-semester="masterDetailProps.selectedSemester"
|
||
v-on="masterDetailEvents"
|
||
/>
|
||
</div>
|
||
|
||
<MntDialogCard
|
||
v-if="!masterDetailProps.isMobile || masterDetailProps.activeMobilePanel === 'master'"
|
||
:dialog-subtitle="masterDetailProps.dialogSubtitle"
|
||
:dialog-title="masterDetailProps.dialogTitle"
|
||
:is-edit-mode="masterDetailProps.isEditMode"
|
||
:is-view-mode="masterDetailProps.isViewMode"
|
||
:width="masterDetailProps.isMobile ? '100%' : 760"
|
||
>
|
||
<template #toolbar>
|
||
<MntRecordNavToolbar
|
||
:has-next-record="masterDetailProps.hasNextRecord"
|
||
:has-prev-record="masterDetailProps.hasPrevRecord"
|
||
:is-edit-mode="masterDetailProps.isEditMode"
|
||
:is-view-mode="masterDetailProps.isViewMode"
|
||
:mobile="masterDetailProps.isMobile"
|
||
v-on="masterDetailEvents"
|
||
/>
|
||
</template>
|
||
<template #content>
|
||
<v-alert
|
||
v-if="masterDetailProps.errorSummary.length > 0 && !masterDetailProps.isLoading"
|
||
class="mb-4"
|
||
type="error"
|
||
variant="tonal"
|
||
>
|
||
<div class="text-subtitle-2 mb-2">請先修正以下問題</div>
|
||
<div class="d-flex flex-column ga-1">
|
||
<v-btn
|
||
v-for="error in masterDetailProps.errorSummary"
|
||
:key="error.field"
|
||
color="error"
|
||
size="small"
|
||
variant="text"
|
||
@click="scrollToField(error.field)"
|
||
>
|
||
{{ error.message }}
|
||
</v-btn>
|
||
</div>
|
||
</v-alert>
|
||
|
||
<v-skeleton-loader
|
||
v-if="masterDetailProps.isLoading"
|
||
class="mt-4"
|
||
type="subtitle,paragraph"
|
||
width="100%"
|
||
/>
|
||
|
||
<v-form
|
||
v-else
|
||
:class="{ 'form-readonly': masterDetailProps.isFormReadonly }"
|
||
@submit.prevent="requestSaveConfirmation()"
|
||
>
|
||
<MasterFileFormFields
|
||
:departments="masterDetailProps.departments"
|
||
:enroll-years="masterDetailProps.enrollYears"
|
||
:field-errors="masterDetailProps.fieldErrors"
|
||
:form="formState.form.value"
|
||
:grade-options="masterDetailProps.gradeOptions"
|
||
:is-form-locked="masterDetailProps.isFormLocked"
|
||
:is-form-readonly="masterDetailProps.isFormReadonly"
|
||
:statuses="masterDetailProps.statuses"
|
||
v-on="masterDetailEvents"
|
||
/>
|
||
|
||
<v-divider />
|
||
|
||
<DetailNavigation
|
||
:is-mobile="masterDetailProps.isMobile"
|
||
:is-view-mode="masterDetailProps.isViewMode"
|
||
:selected-semester-id="masterDetailProps.selectedSemesterId"
|
||
:semesters="masterDetailProps.semesters"
|
||
v-on="masterDetailEvents"
|
||
/>
|
||
</v-form>
|
||
</template>
|
||
<template #actions>
|
||
<template v-if="masterDetailProps.isMobile">
|
||
<v-btn :disabled="masterDetailProps.isSaving" variant="text" @click="flow.requestCloseDialog()">取消</v-btn>
|
||
<v-btn
|
||
v-if="masterDetailProps.isEditMode"
|
||
color="error"
|
||
:disabled="masterDetailProps.isSaving"
|
||
variant="tonal"
|
||
@click="flow.requestDeleteCurrent()"
|
||
>
|
||
刪除
|
||
</v-btn>
|
||
<v-btn
|
||
v-if="!masterDetailProps.isViewMode"
|
||
color="primary"
|
||
:disabled="!masterDetailProps.isDirty || masterDetailProps.isLoading"
|
||
:loading="masterDetailProps.isSaving"
|
||
variant="flat"
|
||
@click="requestSaveConfirmation()"
|
||
>
|
||
儲存
|
||
</v-btn>
|
||
<v-btn v-else color="primary" variant="flat" @click="flow.requestCloseDialog()">關閉</v-btn>
|
||
</template>
|
||
<template v-else>
|
||
<v-spacer />
|
||
<v-btn :disabled="masterDetailProps.isSaving" variant="text" @click="flow.requestCloseDialog()">取消</v-btn>
|
||
<v-btn
|
||
v-if="masterDetailProps.isEditMode"
|
||
color="error"
|
||
:disabled="masterDetailProps.isSaving"
|
||
variant="tonal"
|
||
@click="flow.requestDeleteCurrent()"
|
||
>
|
||
刪除
|
||
</v-btn>
|
||
<v-btn
|
||
v-if="!masterDetailProps.isViewMode"
|
||
color="primary"
|
||
:disabled="!masterDetailProps.isDirty || masterDetailProps.isLoading"
|
||
:loading="masterDetailProps.isSaving"
|
||
variant="flat"
|
||
@click="requestSaveConfirmation()"
|
||
>
|
||
儲存
|
||
</v-btn>
|
||
<v-btn v-else color="primary" variant="flat" @click="flow.requestCloseDialog()">關閉</v-btn>
|
||
</template>
|
||
</template>
|
||
</MntDialogCard>
|
||
</div>
|
||
</v-overlay>
|
||
</teleport>
|
||
|
||
<ConfirmDialog
|
||
:model-value="masterDetailProps.confirmCloseVisible"
|
||
confirm-color="error"
|
||
confirm-text="關閉不儲存"
|
||
message="目前有尚未儲存的內容,確定要關閉嗎?"
|
||
title="未儲存變更"
|
||
@confirm="flow.confirmClose()"
|
||
@update:model-value="flow.confirmCloseVisible.value = $event"
|
||
/>
|
||
|
||
<ConfirmDialog
|
||
:model-value="masterDetailProps.confirmSaveVisible"
|
||
:confirm-loading="masterDetailProps.isSaving"
|
||
confirm-text="確認儲存"
|
||
max-width="520"
|
||
title="確認儲存變更"
|
||
@confirm="confirmSave()"
|
||
@update:model-value="flow.confirmSaveVisible.value = $event"
|
||
>
|
||
<div v-if="masterDetailProps.saveSummary.length > 0" class="d-flex flex-column ga-2">
|
||
<div v-for="item in masterDetailProps.saveSummary" :key="item.label" class="d-flex flex-column">
|
||
<div class="text-caption text-medium-emphasis">{{ item.label }}</div>
|
||
<div v-if="item.before !== null" class="text-body-2">
|
||
<span class="text-medium-emphasis">原:</span>
|
||
<span :class="{ 'text-medium-emphasis': item.before === '—' }">
|
||
{{ item.before }}
|
||
</span>
|
||
</div>
|
||
<div class="text-body-2">
|
||
<span class="text-medium-emphasis">新:</span>
|
||
<span :class="{ 'text-medium-emphasis': item.after === '—' }">
|
||
{{ item.after }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-else class="text-body-2">目前沒有可儲存的變更。</div>
|
||
</ConfirmDialog>
|
||
|
||
<ConfirmDialog
|
||
:model-value="masterDetailProps.confirmDeleteVisible"
|
||
confirm-color="error"
|
||
confirm-text="確定刪除"
|
||
:message="`確定要刪除 ${masterDetailProps.pendingDeleteLabel} 嗎?此操作無法復原。`"
|
||
title="確認刪除"
|
||
@confirm="flow.confirmDelete()"
|
||
@update:model-value="flow.confirmDeleteVisible.value = $event"
|
||
/>
|
||
|
||
<ConfirmDialog
|
||
:model-value="masterDetailProps.confirmSwitchVisible"
|
||
confirm-text="確定切換"
|
||
max-width="480"
|
||
message="目前有尚未儲存的內容,切換為檢視模式將會捨棄變更,確定要切換嗎?"
|
||
title="未儲存變更"
|
||
@confirm="flow.confirmSwitch()"
|
||
@update:model-value="flow.confirmSwitchVisible.value = $event"
|
||
/>
|
||
|
||
<ConfirmDialog
|
||
:model-value="masterDetailProps.confirmNavigateVisible"
|
||
confirm-text="確定切換"
|
||
max-width="480"
|
||
message="目前有尚未儲存的內容,切換到其他資料將會捨棄變更,確定要切換嗎?"
|
||
title="未儲存變更"
|
||
@confirm="flow.confirmNavigate()"
|
||
@update:model-value="flow.confirmNavigateVisible.value = $event"
|
||
/>
|
||
|
||
<v-snackbar v-model="snackbarVisible" color="success" location="bottom right" :timeout="2200">
|
||
儲存成功
|
||
</v-snackbar>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.dialog-overlay :deep(.v-overlay__content) {
|
||
height: 100vh;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
width: 100vw;
|
||
max-width: 100vw;
|
||
}
|
||
|
||
.dialog-panel {
|
||
width: auto;
|
||
max-width: 100%;
|
||
height: 100vh;
|
||
background: transparent;
|
||
padding: 0;
|
||
display: flex;
|
||
}
|
||
|
||
.dialog-panel > .v-card {
|
||
box-shadow: -4px 0 16px rgba(0, 0, 0, 0.12);
|
||
}
|
||
|
||
.detail-panel-wrapper {
|
||
width: 0;
|
||
opacity: 0;
|
||
overflow: hidden;
|
||
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||
margin-right: 0;
|
||
}
|
||
|
||
.detail-panel-wrapper.is-active {
|
||
width: 600px;
|
||
opacity: 1;
|
||
margin-right: 0;
|
||
}
|
||
|
||
.dialog-panel.is-mobile {
|
||
width: 100%;
|
||
}
|
||
|
||
.dialog-panel.is-mobile :deep(.dialog-title) {
|
||
padding: 16px 20px 12px;
|
||
}
|
||
|
||
.dialog-panel.is-mobile :deep(.dialog-toolbar) {
|
||
padding: 8px 12px;
|
||
}
|
||
|
||
.dialog-panel.is-mobile :deep(.dialog-actions) {
|
||
gap: 8px;
|
||
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
|
||
}
|
||
|
||
.dialog-panel.is-mobile :deep(.dialog-actions .v-btn) {
|
||
flex: 1 1 0;
|
||
min-width: 0;
|
||
}
|
||
|
||
.dialog-panel.is-mobile :deep(.v-card-text) {
|
||
padding-bottom: 88px;
|
||
}
|
||
|
||
.detail-panel-wrapper.is-mobile {
|
||
width: 100%;
|
||
opacity: 1;
|
||
overflow: visible;
|
||
}
|
||
|
||
.detail-panel-wrapper.is-mobile.is-active {
|
||
width: 100%;
|
||
}
|
||
|
||
.form-readonly :deep(.v-field) {
|
||
pointer-events: none;
|
||
}
|
||
|
||
@media (max-width: 600px) {
|
||
.dialog-panel {
|
||
width: 100%;
|
||
}
|
||
|
||
.dialog-panel > .v-card {
|
||
width: 100%;
|
||
box-shadow: none;
|
||
}
|
||
}
|
||
</style>
|