96b96bcaaa
Split current project diagnostics into a dedicated analysis document and trim the main architecture strategy to focus on core guidance. This makes the documentation easier to navigate and separates observed issues from recommended architectural principles.docs: reorganize architecture strategy documentation Split current project diagnostics into a dedicated analysis document and trim the main architecture strategy to focus on core guidance. This makes the documentation easier to navigate and separates observed issues from recommended architectural principles.
283 lines
8.6 KiB
Vue
283 lines
8.6 KiB
Vue
<script setup lang="ts">
|
||
import ConfirmDialog from '@/components/maint/CommonConfirmDialog.vue'
|
||
import MntDialogCard from '@/components/maint/MntDialogCard.vue'
|
||
import MntRecordNavToolbar from '@/components/maint/MntRecordNavToolbar.vue'
|
||
import ItemFormFieldGroup from '@/components/items/ItemFormFieldGroup.vue'
|
||
import type {
|
||
SaveSummaryItem,
|
||
StudentFormState,
|
||
} from '@/composables/maint/useStudentMaintenanceForm'
|
||
|
||
interface FieldErrorItem {
|
||
field: string
|
||
message: string
|
||
}
|
||
|
||
interface GradeOption {
|
||
title: string
|
||
value: number
|
||
}
|
||
|
||
defineProps<{
|
||
confirmCloseVisible: boolean
|
||
confirmDeleteVisible: boolean
|
||
confirmNavigateVisible: boolean
|
||
confirmSaveVisible: boolean
|
||
confirmSwitchVisible: boolean
|
||
departments: string[]
|
||
dialogSubtitle: string
|
||
dialogTitle: string
|
||
dialogVisible: boolean
|
||
enrollYears: number[]
|
||
errorSummary: FieldErrorItem[]
|
||
fieldErrors: Record<keyof StudentFormState, string[]>
|
||
gradeOptions: GradeOption[]
|
||
hasNextRecord: boolean
|
||
hasPrevRecord: boolean
|
||
isDirty: boolean
|
||
isEditMode: boolean
|
||
isFormLocked: boolean
|
||
isFormReadonly: boolean
|
||
isLoading: boolean
|
||
isSaving: boolean
|
||
isViewMode: boolean
|
||
pendingDeleteLabel: string
|
||
saveSummary: SaveSummaryItem[]
|
||
statuses: string[]
|
||
}>()
|
||
|
||
const form = defineModel<StudentFormState>('form', { required: true })
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'update:confirmCloseVisible', value: boolean): void
|
||
(e: 'update:confirmDeleteVisible', value: boolean): void
|
||
(e: 'update:confirmNavigateVisible', value: boolean): void
|
||
(e: 'update:confirmSaveVisible', value: boolean): void
|
||
(e: 'update:confirmSwitchVisible', value: boolean): void
|
||
(e: 'dialog-visible-change', value: boolean): void
|
||
(e: 'clear-field-error', field: keyof StudentFormState): void
|
||
(e: 'close'): void
|
||
(e: 'confirm-close'): void
|
||
(e: 'confirm-delete'): void
|
||
(e: 'confirm-navigate'): void
|
||
(e: 'confirm-save'): void
|
||
(e: 'confirm-switch'): void
|
||
(e: 'delete-current'): void
|
||
(e: 'first'): void
|
||
(e: 'last'): void
|
||
(e: 'next'): void
|
||
(e: 'prev'): void
|
||
(e: 'save'): void
|
||
(e: 'scroll-to-field', field: string): void
|
||
(e: 'switch-to-edit'): void
|
||
(e: 'switch-to-view'): void
|
||
}>()
|
||
</script>
|
||
|
||
<template>
|
||
<teleport to="body">
|
||
<v-overlay
|
||
class="dialog-overlay"
|
||
:close-on-content-click="false"
|
||
:model-value="dialogVisible"
|
||
scrim="rgba(0, 0, 0, 0.45)"
|
||
scroll-strategy="block"
|
||
@update:model-value="emit('dialog-visible-change', $event)"
|
||
>
|
||
<div class="dialog-panel">
|
||
<MntDialogCard
|
||
content-class="pa-2 flex-grow-1 overflow-y-auto"
|
||
:dialog-subtitle="dialogSubtitle"
|
||
:dialog-title="dialogTitle"
|
||
:is-edit-mode="isEditMode"
|
||
:is-view-mode="isViewMode"
|
||
width="100%"
|
||
>
|
||
<template #toolbar>
|
||
<MntRecordNavToolbar
|
||
edit-label="進入編輯"
|
||
first-label="第一筆"
|
||
:has-next-record="hasNextRecord"
|
||
:has-prev-record="hasPrevRecord"
|
||
:is-edit-mode="isEditMode"
|
||
:is-view-mode="isViewMode"
|
||
last-label="最後一筆"
|
||
view-label="回到檢視"
|
||
@first="emit('first')"
|
||
@last="emit('last')"
|
||
@next="emit('next')"
|
||
@prev="emit('prev')"
|
||
@switch-to-edit="emit('switch-to-edit')"
|
||
@switch-to-view="emit('switch-to-view')"
|
||
/>
|
||
</template>
|
||
<template #content>
|
||
<v-alert
|
||
v-if="errorSummary.length > 0 && !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 errorSummary"
|
||
:key="error.field"
|
||
color="error"
|
||
size="small"
|
||
variant="text"
|
||
@click="emit('scroll-to-field', error.field)"
|
||
>
|
||
{{ error.message }}
|
||
</v-btn>
|
||
</div>
|
||
</v-alert>
|
||
|
||
<v-skeleton-loader
|
||
v-if="isLoading"
|
||
class="mt-4"
|
||
type="subtitle,paragraph"
|
||
width="100%"
|
||
/>
|
||
|
||
<v-form
|
||
v-else
|
||
:class="{ 'form-readonly': isFormReadonly }"
|
||
@submit.prevent="emit('save')"
|
||
>
|
||
<ItemFormFieldGroup
|
||
v-model="form"
|
||
:departments="departments"
|
||
:enroll-years="enrollYears"
|
||
:field-errors="fieldErrors"
|
||
:grade-options="gradeOptions"
|
||
:is-form-locked="isFormLocked"
|
||
:is-form-readonly="isFormReadonly"
|
||
:statuses="statuses"
|
||
@clear-field-error="emit('clear-field-error', $event)"
|
||
/>
|
||
</v-form>
|
||
</template>
|
||
<template #actions>
|
||
<v-spacer />
|
||
<v-btn :disabled="isSaving" variant="text" @click="emit('close')">取消</v-btn>
|
||
<v-btn
|
||
v-if="isEditMode"
|
||
color="error"
|
||
:disabled="isSaving"
|
||
variant="tonal"
|
||
@click="emit('delete-current')"
|
||
>
|
||
刪除
|
||
</v-btn>
|
||
<v-btn
|
||
v-if="!isViewMode"
|
||
color="primary"
|
||
:disabled="!isDirty || isLoading"
|
||
:loading="isSaving"
|
||
variant="flat"
|
||
@click="emit('save')"
|
||
>
|
||
儲存
|
||
</v-btn>
|
||
<v-btn v-else color="primary" variant="flat" @click="emit('close')">關閉</v-btn>
|
||
</template>
|
||
</MntDialogCard>
|
||
</div>
|
||
</v-overlay>
|
||
</teleport>
|
||
|
||
<ConfirmDialog
|
||
:model-value="confirmCloseVisible"
|
||
confirm-color="error"
|
||
confirm-text="關閉不儲存"
|
||
message="目前有尚未儲存的內容,確定要關閉嗎?"
|
||
title="未儲存變更"
|
||
@confirm="emit('confirm-close')"
|
||
@update:model-value="emit('update:confirmCloseVisible', $event)"
|
||
/>
|
||
|
||
<ConfirmDialog
|
||
:model-value="confirmSaveVisible"
|
||
:confirm-loading="isSaving"
|
||
confirm-text="確認儲存"
|
||
max-width="520"
|
||
title="確認儲存變更"
|
||
@confirm="emit('confirm-save')"
|
||
@update:model-value="emit('update:confirmSaveVisible', $event)"
|
||
>
|
||
<div v-if="saveSummary.length > 0" class="d-flex flex-column ga-2">
|
||
<div v-for="item in 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="confirmDeleteVisible"
|
||
confirm-color="error"
|
||
confirm-text="確定刪除"
|
||
:message="`確定要刪除 ${pendingDeleteLabel} 嗎?此操作無法復原。`"
|
||
title="確認刪除"
|
||
@confirm="emit('confirm-delete')"
|
||
@update:model-value="emit('update:confirmDeleteVisible', $event)"
|
||
/>
|
||
|
||
<ConfirmDialog
|
||
:model-value="confirmSwitchVisible"
|
||
confirm-text="確定切換"
|
||
max-width="480"
|
||
message="目前有尚未儲存的內容,切換為檢視模式將會捨棄變更,確定要切換嗎?"
|
||
title="未儲存變更"
|
||
@confirm="emit('confirm-switch')"
|
||
@update:model-value="emit('update:confirmSwitchVisible', $event)"
|
||
/>
|
||
|
||
<ConfirmDialog
|
||
:model-value="confirmNavigateVisible"
|
||
confirm-text="確定切換"
|
||
max-width="480"
|
||
message="目前有尚未儲存的內容,切換到其他資料將會捨棄變更,確定要切換嗎?"
|
||
title="未儲存變更"
|
||
@confirm="emit('confirm-navigate')"
|
||
@update:model-value="emit('update:confirmNavigateVisible', $event)"
|
||
/>
|
||
</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: 760px;
|
||
max-width: 100%;
|
||
height: 100vh;
|
||
background: rgb(var(--v-theme-surface));
|
||
padding: 12px;
|
||
box-shadow: -12px 0 24px rgba(0, 0, 0, 0.18);
|
||
display: flex;
|
||
}
|
||
|
||
.form-readonly :deep(.v-field) {
|
||
pointer-events: none;
|
||
}
|
||
</style>
|