refactor: update row density to compact in various components for improved layout
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-card class="bg-surface mb-4" v-bind="$attrs">
|
<v-card class="bg-surface mb-4" v-bind="$attrs">
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-row dense>
|
<v-row density="compact">
|
||||||
<!-- Dynamic Search Fields -->
|
<!-- Dynamic Search Fields -->
|
||||||
<v-col
|
<v-col
|
||||||
v-for="field in visibleFields"
|
v-for="field in visibleFields"
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
:lg="field.meta?.lg || field.lg"
|
:lg="field.meta?.lg || field.lg"
|
||||||
:md="field.meta?.md || field.md"
|
:md="field.meta?.md || field.md"
|
||||||
>
|
>
|
||||||
<v-row class="ma-0" dense>
|
<v-row class="ma-0" density="compact">
|
||||||
<v-col class="d-flex align-center justify-start justify-md-end" cols="12" md="4">
|
<v-col class="d-flex align-center justify-start justify-md-end" cols="12" md="4">
|
||||||
<span class="text-body-1">{{ field.label }}</span>
|
<span class="text-body-1">{{ field.label }}</span>
|
||||||
</v-col>
|
</v-col>
|
||||||
@@ -132,11 +132,11 @@ for (const field of props.fields) {
|
|||||||
searchState[field.key] = field.type === 'select' ? null : ''
|
searchState[field.key] = field.type === 'select' ? null : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSearch () {
|
function handleSearch() {
|
||||||
emit('search', { ...searchState })
|
emit('search', { ...searchState })
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleReset () {
|
function handleReset() {
|
||||||
// Reset all fields
|
// Reset all fields
|
||||||
for (const field of props.fields) {
|
for (const field of props.fields) {
|
||||||
searchState[field.key] = field.type === 'select' ? null : ''
|
searchState[field.key] = field.type === 'select' ? null : ''
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
{{ title }}
|
{{ title }}
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-card-text class="pa-4">
|
<v-card-text class="pa-4">
|
||||||
<v-row dense>
|
<v-row density="compact">
|
||||||
<v-col v-for="(nav, i) in navs" :key="i" class="text-center mb-2" cols="4">
|
<v-col v-for="(nav, i) in navs" :key="i" class="text-center mb-2" cols="4">
|
||||||
<v-btn
|
<v-btn
|
||||||
class="mb-1"
|
class="mb-1"
|
||||||
|
|||||||
@@ -8,14 +8,16 @@
|
|||||||
</v-chip>
|
</v-chip>
|
||||||
<v-chip color="info" variant="tonal">筆數 {{ filteredStudents.length }}</v-chip>
|
<v-chip color="info" variant="tonal">筆數 {{ filteredStudents.length }}</v-chip>
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
<v-btn flat :prepend-icon="mdiMagnify" @click="isSearchVisible = !isSearchVisible">條件搜尋</v-btn>
|
<v-btn flat :prepend-icon="mdiMagnify" @click="isSearchVisible = !isSearchVisible"
|
||||||
|
>條件搜尋</v-btn
|
||||||
|
>
|
||||||
<v-switch v-model="isBulkEditEnabled" color="primary" hide-details label="啟用編輯" />
|
<v-switch v-model="isBulkEditEnabled" color="primary" hide-details label="啟用編輯" />
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
|
|
||||||
<v-divider />
|
<v-divider />
|
||||||
|
|
||||||
<v-card-text class="pb-0 pt-2">
|
<v-card-text class="pb-0 pt-2">
|
||||||
<v-row v-if="isSearchVisible" class="mb-2" dense>
|
<v-row v-if="isSearchVisible" class="mb-2" density="compact">
|
||||||
<v-col cols="12" md="3">
|
<v-col cols="12" md="3">
|
||||||
<div class="text-body-1 text-medium-emphasis pl-2">學號</div>
|
<div class="text-body-1 text-medium-emphasis pl-2">學號</div>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
@@ -52,7 +54,7 @@
|
|||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<v-row v-if="isBulkEditEnabled" align="center" class="mb-2 ga-1" dense>
|
<v-row v-if="isBulkEditEnabled" align="center" class="mb-2 ga-1" density="compact">
|
||||||
<v-btn
|
<v-btn
|
||||||
:disabled="!hasSelectedRows"
|
:disabled="!hasSelectedRows"
|
||||||
:prepend-icon="mdiDelete"
|
:prepend-icon="mdiDelete"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-row dense>
|
<v-row density="compact">
|
||||||
<v-col cols="12" md="3">
|
<v-col cols="12" md="3">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
id="field-studentId"
|
id="field-studentId"
|
||||||
|
|||||||
@@ -5,12 +5,22 @@
|
|||||||
<span class="text-h6">{{ title }}</span>
|
<span class="text-h6">{{ title }}</span>
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
<v-btn
|
<v-btn
|
||||||
:icon="mdAndUp ? false : mdiMagnify" :prepend-icon="mdAndUp ? mdiMagnify : undefined" size="small"
|
:icon="mdAndUp ? false : mdiMagnify"
|
||||||
:text="mdAndUp ? '搜尋條件' : false" variant="text" @click="$emit('toggle-search')">
|
:prepend-icon="mdAndUp ? mdiMagnify : undefined"
|
||||||
|
size="small"
|
||||||
|
:text="mdAndUp ? '搜尋條件' : false"
|
||||||
|
variant="text"
|
||||||
|
@click="$emit('toggle-search')"
|
||||||
|
>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
color="primary" :icon="mdAndUp ? false : mdiPlus" :prepend-icon="mdAndUp ? mdiPlus : undefined"
|
color="primary"
|
||||||
size="small" :text="mdAndUp ? createLabel : false" @click="$emit('create')">
|
:icon="mdAndUp ? false : mdiPlus"
|
||||||
|
:prepend-icon="mdAndUp ? mdiPlus : undefined"
|
||||||
|
size="small"
|
||||||
|
:text="mdAndUp ? createLabel : false"
|
||||||
|
@click="$emit('create')"
|
||||||
|
>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
|
|
||||||
@@ -18,7 +28,7 @@ color="primary" :icon="mdAndUp ? false : mdiPlus" :prepend-icon="mdAndUp ? mdiPl
|
|||||||
<template v-if="mdAndUp">
|
<template v-if="mdAndUp">
|
||||||
<v-divider />
|
<v-divider />
|
||||||
<v-card-text v-show="searchPanelOpen" class="px-2 py-1">
|
<v-card-text v-show="searchPanelOpen" class="px-2 py-1">
|
||||||
<v-row dense>
|
<v-row density="compact">
|
||||||
<slot name="search-fields" />
|
<slot name="search-fields" />
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
@@ -29,7 +39,11 @@ color="primary" :icon="mdAndUp ? false : mdiPlus" :prepend-icon="mdAndUp ? mdiPl
|
|||||||
</v-sheet>
|
</v-sheet>
|
||||||
|
|
||||||
<!-- 手機 / Tablet:Bottom Sheet -->
|
<!-- 手機 / Tablet:Bottom Sheet -->
|
||||||
<v-bottom-sheet v-if="!mdAndUp" :model-value="searchPanelOpen" @update:model-value="$emit('toggle-search')">
|
<v-bottom-sheet
|
||||||
|
v-if="!mdAndUp"
|
||||||
|
:model-value="searchPanelOpen"
|
||||||
|
@update:model-value="$emit('toggle-search')"
|
||||||
|
>
|
||||||
<v-card rounded="t-xl">
|
<v-card rounded="t-xl">
|
||||||
<v-card-title class="d-flex align-center py-3 px-4">
|
<v-card-title class="d-flex align-center py-3 px-4">
|
||||||
<span class="text-subtitle-1 font-weight-medium">搜尋條件</span>
|
<span class="text-subtitle-1 font-weight-medium">搜尋條件</span>
|
||||||
@@ -38,7 +52,7 @@ color="primary" :icon="mdAndUp ? false : mdiPlus" :prepend-icon="mdAndUp ? mdiPl
|
|||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-divider />
|
<v-divider />
|
||||||
<v-card-text class="px-2 py-1">
|
<v-card-text class="px-2 py-1">
|
||||||
<v-row dense>
|
<v-row density="compact">
|
||||||
<slot name="search-fields" />
|
<slot name="search-fields" />
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|||||||
@@ -3,12 +3,19 @@
|
|||||||
<v-icon start :icon="mdiSchool" />
|
<v-icon start :icon="mdiSchool" />
|
||||||
子檔資料示範 ({{ semesters.length }})
|
子檔資料示範 ({{ semesters.length }})
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
<v-btn v-if="!isViewMode" color="primary" :prepend-icon="mdiPlus" size="small" variant="text" @click="$emit('add')">
|
<v-btn
|
||||||
|
v-if="!isViewMode"
|
||||||
|
color="primary"
|
||||||
|
:prepend-icon="mdiPlus"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="$emit('add')"
|
||||||
|
>
|
||||||
新增學期
|
新增學期
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-row dense>
|
<v-row density="compact">
|
||||||
<v-col v-for="semester in semesters" :key="semester.id" cols="12" :md="isMobile ? 12 : 6">
|
<v-col v-for="semester in semesters" :key="semester.id" cols="12" :md="isMobile ? 12 : 6">
|
||||||
<v-card
|
<v-card
|
||||||
class="cursor-pointer mb-2"
|
class="cursor-pointer mb-2"
|
||||||
@@ -18,7 +25,9 @@
|
|||||||
@click="$emit('select', semester.id)"
|
@click="$emit('select', semester.id)"
|
||||||
>
|
>
|
||||||
<v-list-item density="compact">
|
<v-list-item density="compact">
|
||||||
<v-list-item-title class="text-body-2 font-weight-bold">{{ semester.semesterName }}</v-list-item-title>
|
<v-list-item-title class="text-body-2 font-weight-bold">{{
|
||||||
|
semester.semesterName
|
||||||
|
}}</v-list-item-title>
|
||||||
<v-list-item-subtitle class="text-caption">
|
<v-list-item-subtitle class="text-caption">
|
||||||
平均: {{ semester.average }} ・ 排名: {{ semester.rank }}
|
平均: {{ semester.average }} ・ 排名: {{ semester.rank }}
|
||||||
</v-list-item-subtitle>
|
</v-list-item-subtitle>
|
||||||
@@ -30,7 +39,10 @@
|
|||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<div v-if="semesters.length === 0" class="text-caption text-center py-6 text-medium-emphasis border border-dashed rounded">
|
<div
|
||||||
|
v-if="semesters.length === 0"
|
||||||
|
class="text-caption text-center py-6 text-medium-emphasis border border-dashed rounded"
|
||||||
|
>
|
||||||
尚無學期資料
|
尚無學期資料
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-card border class="d-flex flex-column h-100 rounded-0" flat width="100%">
|
<v-card border class="d-flex flex-column h-100 rounded-0" flat width="100%">
|
||||||
<v-toolbar v-if="!isDetailEditing" color="transparent" density="compact" flat>
|
<v-toolbar v-if="!isDetailEditing" color="transparent" density="compact" flat>
|
||||||
<v-btn :icon="isMobile ? mdiArrowLeft : mdiClose" size="small" variant="text" @click="$emit('close')" />
|
<v-btn
|
||||||
|
:icon="isMobile ? mdiArrowLeft : mdiClose"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="$emit('close')"
|
||||||
|
/>
|
||||||
<v-toolbar-title class="text-subtitle-1 font-weight-bold">
|
<v-toolbar-title class="text-subtitle-1 font-weight-bold">
|
||||||
{{ selectedSemester ? selectedSemester.semesterName : '學期明細' }}
|
{{ selectedSemester ? selectedSemester.semesterName : '學期明細' }}
|
||||||
</v-toolbar-title>
|
</v-toolbar-title>
|
||||||
@@ -29,10 +34,13 @@
|
|||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
|
|
||||||
<v-toolbar v-else density="compact" flat>
|
<v-toolbar v-else density="compact" flat>
|
||||||
<v-btn :icon="isMobile ? mdiArrowLeft : mdiClose" size="small" variant="text" @click="$emit('cancel-edit')" />
|
<v-btn
|
||||||
<v-toolbar-title class="text-subtitle-1 font-weight-bold">
|
:icon="isMobile ? mdiArrowLeft : mdiClose"
|
||||||
編輯學期
|
size="small"
|
||||||
</v-toolbar-title>
|
variant="text"
|
||||||
|
@click="$emit('cancel-edit')"
|
||||||
|
/>
|
||||||
|
<v-toolbar-title class="text-subtitle-1 font-weight-bold"> 編輯學期 </v-toolbar-title>
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
<v-btn variant="text" @click="$emit('save-edit')">儲存</v-btn>
|
<v-btn variant="text" @click="$emit('save-edit')">儲存</v-btn>
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
@@ -59,7 +67,12 @@
|
|||||||
<v-divider />
|
<v-divider />
|
||||||
|
|
||||||
<div v-if="isMobile" class="pa-3 d-flex flex-column ga-3">
|
<div v-if="isMobile" class="pa-3 d-flex flex-column ga-3">
|
||||||
<v-card v-for="course in selectedSemester.courses" :key="course.code" class="pa-3" variant="outlined">
|
<v-card
|
||||||
|
v-for="course in selectedSemester.courses"
|
||||||
|
:key="course.code"
|
||||||
|
class="pa-3"
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
<div class="d-flex align-start justify-space-between ga-3">
|
<div class="d-flex align-start justify-space-between ga-3">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-body-1 font-weight-medium">{{ course.name }}</div>
|
<div class="text-body-1 font-weight-medium">{{ course.name }}</div>
|
||||||
@@ -73,7 +86,10 @@
|
|||||||
<div>學分 {{ course.credits }}</div>
|
<div>學分 {{ course.credits }}</div>
|
||||||
</div>
|
</div>
|
||||||
</v-card>
|
</v-card>
|
||||||
<div v-if="selectedSemester.courses.length === 0" class="text-center text-medium-emphasis py-6">
|
<div
|
||||||
|
v-if="selectedSemester.courses.length === 0"
|
||||||
|
class="text-center text-medium-emphasis py-6"
|
||||||
|
>
|
||||||
尚無課程資料
|
尚無課程資料
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,7 +109,10 @@
|
|||||||
<div class="text-caption text-medium-emphasis">{{ course.code }}</div>
|
<div class="text-caption text-medium-emphasis">{{ course.code }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">{{ course.credits }}</td>
|
<td class="text-center">{{ course.credits }}</td>
|
||||||
<td class="text-right font-weight-bold" :class="course.score < 60 ? 'text-error' : 'text-success'">
|
<td
|
||||||
|
class="text-right font-weight-bold"
|
||||||
|
:class="course.score < 60 ? 'text-error' : 'text-success'"
|
||||||
|
>
|
||||||
{{ course.score }}
|
{{ course.score }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -104,7 +123,7 @@
|
|||||||
|
|
||||||
<v-card-text v-else class="pa-0 flex-grow-1 overflow-y-auto bg-surface">
|
<v-card-text v-else class="pa-0 flex-grow-1 overflow-y-auto bg-surface">
|
||||||
<div v-if="detailForm" class="pa-4 d-flex flex-column ga-4">
|
<div v-if="detailForm" class="pa-4 d-flex flex-column ga-4">
|
||||||
<v-row dense>
|
<v-row density="compact">
|
||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
:model-value="detailForm.semesterName"
|
:model-value="detailForm.semesterName"
|
||||||
@@ -141,17 +160,34 @@
|
|||||||
<div class="d-flex align-center mt-2 mb-1">
|
<div class="d-flex align-center mt-2 mb-1">
|
||||||
<div class="text-subtitle-2 font-weight-bold">課程列表</div>
|
<div class="text-subtitle-2 font-weight-bold">課程列表</div>
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
<v-btn color="primary" :prepend-icon="mdiPlus" size="small" variant="text" @click="addCourse">
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
:prepend-icon="mdiPlus"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="addCourse"
|
||||||
|
>
|
||||||
加入課程
|
加入課程
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isMobile" class="d-flex flex-column ga-3">
|
<div v-if="isMobile" class="d-flex flex-column ga-3">
|
||||||
<v-card v-for="(course, idx) in detailForm.courses" :key="idx" class="pa-3" variant="outlined">
|
<v-card
|
||||||
|
v-for="(course, idx) in detailForm.courses"
|
||||||
|
:key="idx"
|
||||||
|
class="pa-3"
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
<div class="d-flex align-center mb-3">
|
<div class="d-flex align-center mb-3">
|
||||||
<div class="text-subtitle-2 font-weight-bold">課程 {{ idx + 1 }}</div>
|
<div class="text-subtitle-2 font-weight-bold">課程 {{ idx + 1 }}</div>
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
<v-btn color="error" :icon="mdiDelete" size="small" variant="text" @click="removeCourse(idx)" />
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
:icon="mdiDelete"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="removeCourse(idx)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-column ga-3">
|
<div class="d-flex flex-column ga-3">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
@@ -190,7 +226,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</v-card>
|
</v-card>
|
||||||
<div v-if="detailForm.courses.length === 0" class="text-center text-medium-emphasis py-4 border border-dashed rounded">
|
<div
|
||||||
|
v-if="detailForm.courses.length === 0"
|
||||||
|
class="text-center text-medium-emphasis py-4 border border-dashed rounded"
|
||||||
|
>
|
||||||
暫無課程,請點擊上方按鈕新增
|
暫無課程,請點擊上方按鈕新增
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -207,7 +246,13 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(course, idx) in detailForm.courses" :key="idx">
|
<tr v-for="(course, idx) in detailForm.courses" :key="idx">
|
||||||
<td class="px-0 text-center">
|
<td class="px-0 text-center">
|
||||||
<v-btn color="error" :icon="mdiDelete" size="small" variant="text" @click="removeCourse(idx)" />
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
:icon="mdiDelete"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="removeCourse(idx)"
|
||||||
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-2">
|
<td class="py-2">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
@@ -251,7 +296,9 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="detailForm.courses.length === 0">
|
<tr v-if="detailForm.courses.length === 0">
|
||||||
<td class="text-center text-medium-emphasis py-4" colspan="4">暫無課程,請點擊上方按鈕新增</td>
|
<td class="text-center text-medium-emphasis py-4" colspan="4">
|
||||||
|
暫無課程,請點擊上方按鈕新增
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</v-table>
|
</v-table>
|
||||||
@@ -285,21 +332,21 @@ const emit = defineEmits<{
|
|||||||
(event: 'update:detail-form', value: SemesterRecord | null): void
|
(event: 'update:detail-form', value: SemesterRecord | null): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
function cloneDetailForm (form: SemesterRecord): SemesterRecord {
|
function cloneDetailForm(form: SemesterRecord): SemesterRecord {
|
||||||
return {
|
return {
|
||||||
...form,
|
...form,
|
||||||
courses: form.courses.map((course) => ({ ...course })),
|
courses: form.courses.map((course) => ({ ...course })),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitDetailFormUpdate (updater: (draft: SemesterRecord) => void) {
|
function emitDetailFormUpdate(updater: (draft: SemesterRecord) => void) {
|
||||||
if (!props.detailForm) return
|
if (!props.detailForm) return
|
||||||
const nextForm = cloneDetailForm(props.detailForm)
|
const nextForm = cloneDetailForm(props.detailForm)
|
||||||
updater(nextForm)
|
updater(nextForm)
|
||||||
emit('update:detail-form', nextForm)
|
emit('update:detail-form', nextForm)
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDetailFormField<K extends keyof SemesterRecord> (key: K, value: SemesterRecord[K]) {
|
function updateDetailFormField<K extends keyof SemesterRecord>(key: K, value: SemesterRecord[K]) {
|
||||||
emitDetailFormUpdate((draft) => {
|
emitDetailFormUpdate((draft) => {
|
||||||
draft[key] = value
|
draft[key] = value
|
||||||
})
|
})
|
||||||
@@ -308,7 +355,7 @@ function updateDetailFormField<K extends keyof SemesterRecord> (key: K, value: S
|
|||||||
function updateCourseField<K extends keyof SemesterRecord['courses'][number]>(
|
function updateCourseField<K extends keyof SemesterRecord['courses'][number]>(
|
||||||
index: number,
|
index: number,
|
||||||
key: K,
|
key: K,
|
||||||
value: SemesterRecord['courses'][number][K],
|
value: SemesterRecord['courses'][number][K]
|
||||||
) {
|
) {
|
||||||
emitDetailFormUpdate((draft) => {
|
emitDetailFormUpdate((draft) => {
|
||||||
const course = draft.courses[index]
|
const course = draft.courses[index]
|
||||||
@@ -317,13 +364,13 @@ function updateCourseField<K extends keyof SemesterRecord['courses'][number]>(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function toNumber (value: unknown): number {
|
function toNumber(value: unknown): number {
|
||||||
if (typeof value === 'number') return value
|
if (typeof value === 'number') return value
|
||||||
const parsed = Number(value)
|
const parsed = Number(value)
|
||||||
return Number.isFinite(parsed) ? parsed : 0
|
return Number.isFinite(parsed) ? parsed : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function addCourse () {
|
function addCourse() {
|
||||||
emitDetailFormUpdate((draft) => {
|
emitDetailFormUpdate((draft) => {
|
||||||
draft.courses.push({
|
draft.courses.push({
|
||||||
code: '',
|
code: '',
|
||||||
@@ -334,18 +381,18 @@ function addCourse () {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeCourse (index: number) {
|
function removeCourse(index: number) {
|
||||||
emitDetailFormUpdate((draft) => {
|
emitDetailFormUpdate((draft) => {
|
||||||
draft.courses.splice(index, 1)
|
draft.courses.splice(index, 1)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalCredits = computed(() =>
|
const totalCredits = computed(
|
||||||
props.selectedSemester?.courses.reduce((sum, course) => sum + course.credits, 0) ?? 0,
|
() => props.selectedSemester?.courses.reduce((sum, course) => sum + course.credits, 0) ?? 0
|
||||||
)
|
)
|
||||||
|
|
||||||
const statsClass = computed(() =>
|
const statsClass = computed(() =>
|
||||||
props.isMobile ? 'pa-3 d-flex flex-column ga-3 bg-surface' : 'pa-3 d-flex ga-3 bg-surface',
|
props.isMobile ? 'pa-3 d-flex flex-column ga-3 bg-surface' : 'pa-3 d-flex ga-3 bg-surface'
|
||||||
)
|
)
|
||||||
|
|
||||||
const statCardClass = computed(() => (props.isMobile ? '' : 'flex-grow-1'))
|
const statCardClass = computed(() => (props.isMobile ? '' : 'flex-grow-1'))
|
||||||
|
|||||||
+36
-17
@@ -1,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container class="pa-0" fluid>
|
<v-container class="pa-0" fluid>
|
||||||
<div class="d-flex flex-column ga-5 py-4 pr-2 pl-0">
|
<div class="d-flex flex-column ga-5 py-4 pr-2 pl-0">
|
||||||
<v-sheet class="d-flex flex-column flex-sm-row align-start align-sm-center ga-4 pa-5 elevation-1" color="surface">
|
<v-sheet
|
||||||
|
class="d-flex flex-column flex-sm-row align-start align-sm-center ga-4 pa-5 elevation-1"
|
||||||
|
color="surface"
|
||||||
|
>
|
||||||
<v-avatar color="primary" size="52" variant="tonal">
|
<v-avatar color="primary" size="52" variant="tonal">
|
||||||
<span class="text-h5">👋</span>
|
<span class="text-h5">👋</span>
|
||||||
</v-avatar>
|
</v-avatar>
|
||||||
@@ -25,11 +28,13 @@
|
|||||||
這裡透過 resolveNewsItem 抽出原始資料,再沿用原本的卡片排版。
|
這裡透過 resolveNewsItem 抽出原始資料,再沿用原本的卡片排版。
|
||||||
-->
|
-->
|
||||||
<template #default="{ items }">
|
<template #default="{ items }">
|
||||||
<v-row dense>
|
<v-row density="compact">
|
||||||
<v-col v-for="wrapped in items" :key="resolveNewsItem(wrapped).id" cols="12">
|
<v-col v-for="wrapped in items" :key="resolveNewsItem(wrapped).id" cols="12">
|
||||||
<v-card
|
<v-card
|
||||||
class="news-item d-flex flex-column flex-sm-row ga-4 pa-4 bg-surface" variant="outlined"
|
class="news-item d-flex flex-column flex-sm-row ga-4 pa-4 bg-surface"
|
||||||
@click="handleNews(resolveNewsItem(wrapped))">
|
variant="outlined"
|
||||||
|
@click="handleNews(resolveNewsItem(wrapped))"
|
||||||
|
>
|
||||||
<v-sheet class="news-badge">
|
<v-sheet class="news-badge">
|
||||||
<div class="news-badge-date">{{ resolveNewsItem(wrapped).date }}</div>
|
<div class="news-badge-date">{{ resolveNewsItem(wrapped).date }}</div>
|
||||||
<div class="news-badge-month">{{ resolveNewsItem(wrapped).month }}</div>
|
<div class="news-badge-month">{{ resolveNewsItem(wrapped).month }}</div>
|
||||||
@@ -38,8 +43,12 @@ class="news-item d-flex flex-column flex-sm-row ga-4 pa-4 bg-surface" variant="o
|
|||||||
<div class="d-flex flex-wrap align-center font-weight-bold">
|
<div class="d-flex flex-wrap align-center font-weight-bold">
|
||||||
{{ resolveNewsItem(wrapped).title }}
|
{{ resolveNewsItem(wrapped).title }}
|
||||||
<v-chip
|
<v-chip
|
||||||
v-if="resolveNewsItem(wrapped).isNew" class="ml-2" color="primary" size="x-small"
|
v-if="resolveNewsItem(wrapped).isNew"
|
||||||
variant="flat">
|
class="ml-2"
|
||||||
|
color="primary"
|
||||||
|
size="x-small"
|
||||||
|
variant="flat"
|
||||||
|
>
|
||||||
NEW
|
NEW
|
||||||
</v-chip>
|
</v-chip>
|
||||||
</div>
|
</div>
|
||||||
@@ -65,8 +74,12 @@ v-if="resolveNewsItem(wrapped).isNew" class="ml-2" color="primary" size="x-small
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<v-card
|
<v-card
|
||||||
class="d-flex align-center justify-space-between ga-3 px-5 py-4" color="secondary" rounded="xl"
|
class="d-flex align-center justify-space-between ga-3 px-5 py-4"
|
||||||
variant="tonal" @click="handleMessageCenter">
|
color="secondary"
|
||||||
|
rounded="xl"
|
||||||
|
variant="tonal"
|
||||||
|
@click="handleMessageCenter"
|
||||||
|
>
|
||||||
<div class="d-flex align-center ga-4">
|
<div class="d-flex align-center ga-4">
|
||||||
<v-avatar color="secondary" size="44" variant="flat">
|
<v-avatar color="secondary" size="44" variant="flat">
|
||||||
<span class="text-h6">✉️</span>
|
<span class="text-h6">✉️</span>
|
||||||
@@ -81,11 +94,13 @@ class="d-flex align-center justify-space-between ga-3 px-5 py-4" color="secondar
|
|||||||
|
|
||||||
<section class="d-flex flex-column pb-4">
|
<section class="d-flex flex-column pb-4">
|
||||||
<div class="d-flex align-center ga-2 text-h6 font-weight-bold">🚀 快速存取</div>
|
<div class="d-flex align-center ga-2 text-h6 font-weight-bold">🚀 快速存取</div>
|
||||||
<v-row class="mt-2" dense>
|
<v-row class="mt-2" density="compact">
|
||||||
<v-col v-for="item in quickItems" :key="item.title" cols="6" md="2" sm="4">
|
<v-col v-for="item in quickItems" :key="item.title" cols="6" md="2" sm="4">
|
||||||
<v-card
|
<v-card
|
||||||
class="d-flex flex-column align-center ga-2 text-center py-4 px-2 quick-item" variant="outlined"
|
class="d-flex flex-column align-center ga-2 text-center py-4 px-2 quick-item"
|
||||||
@click="handleQuick(item)">
|
variant="outlined"
|
||||||
|
@click="handleQuick(item)"
|
||||||
|
>
|
||||||
<div class="text-h5">{{ item.icon }}</div>
|
<div class="text-h5">{{ item.icon }}</div>
|
||||||
<div class="text-body-2 font-weight-medium">{{ item.title }}</div>
|
<div class="text-body-2 font-weight-medium">{{ item.title }}</div>
|
||||||
</v-card>
|
</v-card>
|
||||||
@@ -175,7 +190,7 @@ const selectedNews = ref<NewsItem | null>(null)
|
|||||||
const isNewsDialogOpen = ref(false)
|
const isNewsDialogOpen = ref(false)
|
||||||
|
|
||||||
// v-data-iterator 會包裝 items,這個方法用來安全地取回原始資料結構。
|
// v-data-iterator 會包裝 items,這個方法用來安全地取回原始資料結構。
|
||||||
function resolveNewsItem (wrapped: unknown): NewsItem {
|
function resolveNewsItem(wrapped: unknown): NewsItem {
|
||||||
if (wrapped && typeof wrapped === 'object' && 'raw' in wrapped) {
|
if (wrapped && typeof wrapped === 'object' && 'raw' in wrapped) {
|
||||||
return (wrapped as { raw: NewsItem }).raw
|
return (wrapped as { raw: NewsItem }).raw
|
||||||
}
|
}
|
||||||
@@ -183,17 +198,17 @@ function resolveNewsItem (wrapped: unknown): NewsItem {
|
|||||||
return wrapped as NewsItem
|
return wrapped as NewsItem
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNews (item: NewsItem) {
|
function handleNews(item: NewsItem) {
|
||||||
selectedNews.value = item
|
selectedNews.value = item
|
||||||
isNewsDialogOpen.value = true
|
isNewsDialogOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 點擊首頁「訊息中心」卡片,開啟共用的訊息清單 dialog
|
// 點擊首頁「訊息中心」卡片,開啟共用的訊息清單 dialog
|
||||||
function handleMessageCenter () {
|
function handleMessageCenter() {
|
||||||
messageStore.open()
|
messageStore.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleQuick (item: (typeof quickItems)[number]) {
|
function handleQuick(item: (typeof quickItems)[number]) {
|
||||||
snackbar.show({ message: `前往:${item.title}`, color: 'info' })
|
snackbar.show({ message: `前往:${item.title}`, color: 'info' })
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -201,7 +216,9 @@ function handleQuick (item: (typeof quickItems)[number]) {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.news-item {
|
.news-item {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
transition:
|
||||||
|
transform 0.2s ease,
|
||||||
|
box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.news-item:hover {
|
.news-item:hover {
|
||||||
@@ -236,7 +253,9 @@ function handleQuick (item: (typeof quickItems)[number]) {
|
|||||||
.quick-item {
|
.quick-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
transition:
|
||||||
|
transform 0.2s ease,
|
||||||
|
box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-item:hover {
|
.quick-item:hover {
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ v-model="confirmDeleteCourseVisible" confirm-color="error"
|
|||||||
<v-select
|
<v-select
|
||||||
v-model="addCourseForm.name" class="mb-3" density="comfortable" :items="availableCourses"
|
v-model="addCourseForm.name" class="mb-3" density="comfortable" :items="availableCourses"
|
||||||
label="課程名稱" variant="outlined" @update:model-value="handleAddCourseNameSelect" />
|
label="課程名稱" variant="outlined" @update:model-value="handleAddCourseNameSelect" />
|
||||||
<v-row dense>
|
<v-row density="compact">
|
||||||
<v-col cols="6">
|
<v-col cols="6">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model.number="addCourseForm.credits" density="comfortable" hide-spin-buttons label="學分"
|
v-model.number="addCourseForm.credits" density="comfortable" hide-spin-buttons label="學分"
|
||||||
|
|||||||
@@ -1,31 +1,62 @@
|
|||||||
<template>
|
<template>
|
||||||
<mnt-page-cards
|
<mnt-page-cards
|
||||||
:search-panel-open="searchPanelOpen" :title="`單筆資料維護示範`"
|
:search-panel-open="searchPanelOpen"
|
||||||
@create="openAddDialog" @toggle-search="searchPanelOpen = !searchPanelOpen">
|
:title="`單筆資料維護示範`"
|
||||||
|
@create="openAddDialog"
|
||||||
|
@toggle-search="searchPanelOpen = !searchPanelOpen"
|
||||||
|
>
|
||||||
<template #search-fields>
|
<template #search-fields>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div class="text-body-2 text-medium-emphasis pl-2">學號</div>
|
<div class="text-body-2 text-medium-emphasis pl-2">學號</div>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="search.studentId" density="compact" hide-details placeholder="例如:S2024001"
|
v-model="search.studentId"
|
||||||
variant="outlined" />
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
placeholder="例如:S2024001"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div class="text-body-2 text-medium-emphasis pl-2">姓名</div>
|
<div class="text-body-2 text-medium-emphasis pl-2">姓名</div>
|
||||||
<v-text-field v-model="search.name" density="compact" hide-details placeholder="例如:王小明" variant="outlined" />
|
<v-text-field
|
||||||
|
v-model="search.name"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
placeholder="例如:王小明"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div class="text-body-2 text-medium-emphasis pl-2">系所</div>
|
<div class="text-body-2 text-medium-emphasis pl-2">系所</div>
|
||||||
<v-select v-model="search.department" density="compact" hide-details :items="departments" variant="outlined" />
|
<v-select
|
||||||
|
v-model="search.department"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
:items="departments"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div class="text-body-2 text-medium-emphasis pl-2">年級</div>
|
<div class="text-body-2 text-medium-emphasis pl-2">年級</div>
|
||||||
<v-select
|
<v-select
|
||||||
v-model="search.grade" density="compact" hide-details item-title="title" item-value="value"
|
v-model="search.grade"
|
||||||
:items="gradeOptions" variant="outlined" />
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
item-title="title"
|
||||||
|
item-value="value"
|
||||||
|
:items="gradeOptions"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div class="text-body-2 text-medium-emphasis pl-2">狀態</div>
|
<div class="text-body-2 text-medium-emphasis pl-2">狀態</div>
|
||||||
<v-select v-model="search.status" density="compact" hide-details :items="statuses" variant="outlined" />
|
<v-select
|
||||||
|
v-model="search.status"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
:items="statuses"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col class="d-flex justify-end align-end flex-grow-1 ga-2" cols="12" md="auto">
|
<v-col class="d-flex justify-end align-end flex-grow-1 ga-2" cols="12" md="auto">
|
||||||
<v-btn :prepend-icon="mdiBroom" variant="text" @click="resetSearch">清除</v-btn>
|
<v-btn :prepend-icon="mdiBroom" variant="text" @click="resetSearch">清除</v-btn>
|
||||||
@@ -34,9 +65,17 @@ v-model="search.grade" density="compact" hide-details item-title="title" item-va
|
|||||||
</template>
|
</template>
|
||||||
<template #table>
|
<template #table>
|
||||||
<v-data-table
|
<v-data-table
|
||||||
class="student-table" density="compact" fixed-header :headers="tableHeaders"
|
class="student-table"
|
||||||
height="100%" :items="students" :items-per-page="10" items-per-page-text="每頁筆數" page-text="第 {0}-{1} 筆 / 共 {2} 筆"
|
density="compact"
|
||||||
:row-props="rowProps">
|
fixed-header
|
||||||
|
:headers="tableHeaders"
|
||||||
|
height="100%"
|
||||||
|
:items="students"
|
||||||
|
:items-per-page="10"
|
||||||
|
items-per-page-text="每頁筆數"
|
||||||
|
page-text="第 {0}-{1} 筆 / 共 {2} 筆"
|
||||||
|
:row-props="rowProps"
|
||||||
|
>
|
||||||
<template #[`item.grade`]="{ item }">
|
<template #[`item.grade`]="{ item }">
|
||||||
{{ gradeLabel(item.grade) }}
|
{{ gradeLabel(item.grade) }}
|
||||||
</template>
|
</template>
|
||||||
@@ -47,15 +86,31 @@ class="student-table" density="compact" fixed-header :headers="tableHeaders"
|
|||||||
</template>
|
</template>
|
||||||
<template #[`item.actions`]="{ item }">
|
<template #[`item.actions`]="{ item }">
|
||||||
<div class="d-flex ga-2">
|
<div class="d-flex ga-2">
|
||||||
<v-btn color="info" :prepend-icon="mdiEye" size="small" variant="text" @click="openViewDialog(item)">
|
<v-btn
|
||||||
|
color="info"
|
||||||
|
:prepend-icon="mdiEye"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="openViewDialog(item)"
|
||||||
|
>
|
||||||
檢視
|
檢視
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn color="primary" :prepend-icon="mdiPencil" size="small" variant="text" @click="openEditDialog(item)">
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
:prepend-icon="mdiPencil"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="openEditDialog(item)"
|
||||||
|
>
|
||||||
修改
|
修改
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
color="error" :prepend-icon="mdiDelete" size="small" variant="text"
|
color="error"
|
||||||
@click="requestDeleteConfirmation(item)">
|
:prepend-icon="mdiDelete"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="requestDeleteConfirmation(item)"
|
||||||
|
>
|
||||||
刪除
|
刪除
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,106 +123,220 @@ color="error" :prepend-icon="mdiDelete" size="small" variant="text"
|
|||||||
<teleport to="body">
|
<teleport to="body">
|
||||||
<!-- 包成元件需要傳高度寬度給dialog-panel -->
|
<!-- 包成元件需要傳高度寬度給dialog-panel -->
|
||||||
<v-overlay
|
<v-overlay
|
||||||
class="dialog-overlay" :close-on-content-click="false" :model-value="dialogVisible" scrim="rgba(0, 0, 0, 0.45)"
|
class="dialog-overlay"
|
||||||
scroll-strategy="block" @update:model-value="handleDialogVisibility">
|
:close-on-content-click="false"
|
||||||
|
:model-value="dialogVisible"
|
||||||
|
scrim="rgba(0, 0, 0, 0.45)"
|
||||||
|
scroll-strategy="block"
|
||||||
|
@update:model-value="handleDialogVisibility"
|
||||||
|
>
|
||||||
<div class="dialog-panel">
|
<div class="dialog-panel">
|
||||||
<mnt-dialog-card
|
<mnt-dialog-card
|
||||||
content-class="pa-2 flex-grow-1 overflow-y-auto" :dialog-subtitle="dialogSubtitle" :dialog-title="dialogTitle"
|
content-class="pa-2 flex-grow-1 overflow-y-auto"
|
||||||
:is-edit-mode="isEditMode" :is-view-mode="isViewMode" width="100%">
|
:dialog-subtitle="dialogSubtitle"
|
||||||
|
:dialog-title="dialogTitle"
|
||||||
|
:is-edit-mode="isEditMode"
|
||||||
|
:is-view-mode="isViewMode"
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<mnt-record-nav-toolbar
|
<mnt-record-nav-toolbar
|
||||||
edit-label="進入編輯" first-label="第一筆"
|
edit-label="進入編輯"
|
||||||
:has-next-record="hasNextRecord" :has-prev-record="hasPrevRecord" :is-edit-mode="isEditMode" :is-view-mode="isViewMode"
|
first-label="第一筆"
|
||||||
last-label="最後一筆" view-label="回到檢視" @first="openEdgeRecord('first')" @last="openEdgeRecord('last')"
|
:has-next-record="hasNextRecord"
|
||||||
@next="openAdjacentRecord('next')" @prev="openAdjacentRecord('prev')" @switch-to-edit="switchToEditMode"
|
:has-prev-record="hasPrevRecord"
|
||||||
@switch-to-view="switchToViewMode" />
|
:is-edit-mode="isEditMode"
|
||||||
|
:is-view-mode="isViewMode"
|
||||||
|
last-label="最後一筆"
|
||||||
|
view-label="回到檢視"
|
||||||
|
@first="openEdgeRecord('first')"
|
||||||
|
@last="openEdgeRecord('last')"
|
||||||
|
@next="openAdjacentRecord('next')"
|
||||||
|
@prev="openAdjacentRecord('prev')"
|
||||||
|
@switch-to-edit="switchToEditMode"
|
||||||
|
@switch-to-view="switchToViewMode"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<!-- 儲存前驗證錯誤摘要 -->
|
<!-- 儲存前驗證錯誤摘要 -->
|
||||||
<v-alert v-if="errorSummary.length > 0 && !isLoading" class="mb-4" type="error" variant="tonal">
|
<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="text-subtitle-2 mb-2">請先修正以下問題</div>
|
||||||
<div class="d-flex flex-column ga-1">
|
<div class="d-flex flex-column ga-1">
|
||||||
<v-btn
|
<v-btn
|
||||||
v-for="error in errorSummary" :key="error.field" color="error" size="small" variant="text"
|
v-for="error in errorSummary"
|
||||||
@click="scrollToField(error.field)">
|
:key="error.field"
|
||||||
|
color="error"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="scrollToField(error.field)"
|
||||||
|
>
|
||||||
{{ error.message }}
|
{{ error.message }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</v-alert>
|
</v-alert>
|
||||||
|
|
||||||
<!-- 編輯/檢視載入中骨架 -->
|
<!-- 編輯/檢視載入中骨架 -->
|
||||||
<v-skeleton-loader v-if="isLoading" class="mt-4" type="subtitle,paragraph" width="100%" />
|
<v-skeleton-loader
|
||||||
|
v-if="isLoading"
|
||||||
|
class="mt-4"
|
||||||
|
type="subtitle,paragraph"
|
||||||
|
width="100%"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 表單:檢視模式使用 readonly,避免 focus 狀態 -->
|
<!-- 表單:檢視模式使用 readonly,避免 focus 狀態 -->
|
||||||
<v-form v-else :class="{ 'form-readonly': isFormReadonly }" @submit.prevent="requestSaveConfirmation">
|
<v-form
|
||||||
<v-row dense>
|
v-else
|
||||||
|
:class="{ 'form-readonly': isFormReadonly }"
|
||||||
|
@submit.prevent="requestSaveConfirmation"
|
||||||
|
>
|
||||||
|
<v-row density="compact">
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
id="field-studentId" v-model="form.studentId" density="comfortable" :disabled="isFormLocked"
|
id="field-studentId"
|
||||||
:error-messages="fieldErrors.studentId" label="學號" placeholder="例如:S2024008"
|
v-model="form.studentId"
|
||||||
:readonly="isFormReadonly" variant="outlined"
|
density="comfortable"
|
||||||
@update:model-value="clearFieldError('studentId')" />
|
:disabled="isFormLocked"
|
||||||
|
:error-messages="fieldErrors.studentId"
|
||||||
|
label="學號"
|
||||||
|
placeholder="例如:S2024008"
|
||||||
|
:readonly="isFormReadonly"
|
||||||
|
variant="outlined"
|
||||||
|
@update:model-value="clearFieldError('studentId')"
|
||||||
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
id="field-name" v-model="form.name" density="comfortable" :disabled="isFormLocked" :error-messages="fieldErrors.name"
|
id="field-name"
|
||||||
label="姓名" placeholder="例如:陳怡君" :readonly="isFormReadonly"
|
v-model="form.name"
|
||||||
variant="outlined" @update:model-value="clearFieldError('name')" />
|
density="comfortable"
|
||||||
|
:disabled="isFormLocked"
|
||||||
|
:error-messages="fieldErrors.name"
|
||||||
|
label="姓名"
|
||||||
|
placeholder="例如:陳怡君"
|
||||||
|
:readonly="isFormReadonly"
|
||||||
|
variant="outlined"
|
||||||
|
@update:model-value="clearFieldError('name')"
|
||||||
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<v-select
|
<v-select
|
||||||
id="field-department" v-model="form.department" density="comfortable" :disabled="isFormLocked"
|
id="field-department"
|
||||||
:error-messages="fieldErrors.department" :items="departments" label="系所"
|
v-model="form.department"
|
||||||
:readonly="isFormReadonly" variant="outlined"
|
density="comfortable"
|
||||||
@update:model-value="clearFieldError('department')" />
|
:disabled="isFormLocked"
|
||||||
|
:error-messages="fieldErrors.department"
|
||||||
|
:items="departments"
|
||||||
|
label="系所"
|
||||||
|
:readonly="isFormReadonly"
|
||||||
|
variant="outlined"
|
||||||
|
@update:model-value="clearFieldError('department')"
|
||||||
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<v-select
|
<v-select
|
||||||
id="field-grade" v-model="form.grade" density="comfortable" :disabled="isFormLocked" :error-messages="fieldErrors.grade"
|
id="field-grade"
|
||||||
item-title="title" item-value="value" :items="gradeOptions" label="年級"
|
v-model="form.grade"
|
||||||
:readonly="isFormReadonly" variant="outlined"
|
density="comfortable"
|
||||||
@update:model-value="clearFieldError('grade')" />
|
:disabled="isFormLocked"
|
||||||
|
:error-messages="fieldErrors.grade"
|
||||||
|
item-title="title"
|
||||||
|
item-value="value"
|
||||||
|
:items="gradeOptions"
|
||||||
|
label="年級"
|
||||||
|
:readonly="isFormReadonly"
|
||||||
|
variant="outlined"
|
||||||
|
@update:model-value="clearFieldError('grade')"
|
||||||
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<v-select
|
<v-select
|
||||||
id="field-enrollYear" v-model="form.enrollYear" density="comfortable" :disabled="isFormLocked"
|
id="field-enrollYear"
|
||||||
:error-messages="fieldErrors.enrollYear" :items="enrollYears" label="入學年度"
|
v-model="form.enrollYear"
|
||||||
:readonly="isFormReadonly" variant="outlined"
|
density="comfortable"
|
||||||
@update:model-value="clearFieldError('enrollYear')" />
|
:disabled="isFormLocked"
|
||||||
|
:error-messages="fieldErrors.enrollYear"
|
||||||
|
:items="enrollYears"
|
||||||
|
label="入學年度"
|
||||||
|
:readonly="isFormReadonly"
|
||||||
|
variant="outlined"
|
||||||
|
@update:model-value="clearFieldError('enrollYear')"
|
||||||
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
id="field-credits" v-model.number="form.credits" density="comfortable" :disabled="isFormLocked" :error-messages="fieldErrors.credits"
|
id="field-credits"
|
||||||
label="已修學分" min="0" :readonly="isFormReadonly"
|
v-model.number="form.credits"
|
||||||
type="number" variant="outlined"
|
density="comfortable"
|
||||||
@update:model-value="clearFieldError('credits')" />
|
:disabled="isFormLocked"
|
||||||
|
:error-messages="fieldErrors.credits"
|
||||||
|
label="已修學分"
|
||||||
|
min="0"
|
||||||
|
:readonly="isFormReadonly"
|
||||||
|
type="number"
|
||||||
|
variant="outlined"
|
||||||
|
@update:model-value="clearFieldError('credits')"
|
||||||
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
id="field-advisor" v-model="form.advisor" density="comfortable" :disabled="isFormLocked"
|
id="field-advisor"
|
||||||
:error-messages="fieldErrors.advisor" label="指導老師" placeholder="例如:林教授"
|
v-model="form.advisor"
|
||||||
:readonly="isFormReadonly" variant="outlined"
|
density="comfortable"
|
||||||
@update:model-value="clearFieldError('advisor')" />
|
:disabled="isFormLocked"
|
||||||
|
:error-messages="fieldErrors.advisor"
|
||||||
|
label="指導老師"
|
||||||
|
placeholder="例如:林教授"
|
||||||
|
:readonly="isFormReadonly"
|
||||||
|
variant="outlined"
|
||||||
|
@update:model-value="clearFieldError('advisor')"
|
||||||
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
id="field-email" v-model="form.email" density="comfortable" :disabled="isFormLocked"
|
id="field-email"
|
||||||
:error-messages="fieldErrors.email" label="Email" placeholder="name@school.edu"
|
v-model="form.email"
|
||||||
:readonly="isFormReadonly" variant="outlined"
|
density="comfortable"
|
||||||
@update:model-value="clearFieldError('email')" />
|
:disabled="isFormLocked"
|
||||||
|
:error-messages="fieldErrors.email"
|
||||||
|
label="Email"
|
||||||
|
placeholder="name@school.edu"
|
||||||
|
:readonly="isFormReadonly"
|
||||||
|
variant="outlined"
|
||||||
|
@update:model-value="clearFieldError('email')"
|
||||||
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
id="field-phone" v-model="form.phone" density="comfortable" :disabled="isFormLocked"
|
id="field-phone"
|
||||||
:error-messages="fieldErrors.phone" label="電話" placeholder="例如:02-2345-6789"
|
v-model="form.phone"
|
||||||
:readonly="isFormReadonly" variant="outlined"
|
density="comfortable"
|
||||||
@update:model-value="clearFieldError('phone')" />
|
:disabled="isFormLocked"
|
||||||
|
:error-messages="fieldErrors.phone"
|
||||||
|
label="電話"
|
||||||
|
placeholder="例如:02-2345-6789"
|
||||||
|
:readonly="isFormReadonly"
|
||||||
|
variant="outlined"
|
||||||
|
@update:model-value="clearFieldError('phone')"
|
||||||
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<v-select
|
<v-select
|
||||||
id="field-status" v-model="form.status" density="comfortable" :disabled="isFormLocked" :error-messages="fieldErrors.status"
|
id="field-status"
|
||||||
:items="statuses" label="狀態" :readonly="isFormReadonly"
|
v-model="form.status"
|
||||||
variant="outlined" @update:model-value="clearFieldError('status')" />
|
density="comfortable"
|
||||||
|
:disabled="isFormLocked"
|
||||||
|
:error-messages="fieldErrors.status"
|
||||||
|
:items="statuses"
|
||||||
|
label="狀態"
|
||||||
|
:readonly="isFormReadonly"
|
||||||
|
variant="outlined"
|
||||||
|
@update:model-value="clearFieldError('status')"
|
||||||
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-form>
|
</v-form>
|
||||||
@@ -175,12 +344,23 @@ id="field-status" v-model="form.status" density="comfortable" :disabled="isFormL
|
|||||||
<template #actions>
|
<template #actions>
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
<v-btn :disabled="isSaving" variant="text" @click="requestCloseDialog">取消</v-btn>
|
<v-btn :disabled="isSaving" variant="text" @click="requestCloseDialog">取消</v-btn>
|
||||||
<v-btn v-if="isEditMode" color="error" :disabled="isSaving" variant="tonal" @click="requestDeleteCurrent">
|
<v-btn
|
||||||
|
v-if="isEditMode"
|
||||||
|
color="error"
|
||||||
|
:disabled="isSaving"
|
||||||
|
variant="tonal"
|
||||||
|
@click="requestDeleteCurrent"
|
||||||
|
>
|
||||||
刪除
|
刪除
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="!isViewMode" color="primary" :disabled="!isDirty || isLoading" :loading="isSaving"
|
v-if="!isViewMode"
|
||||||
variant="flat" @click="requestSaveConfirmation">
|
color="primary"
|
||||||
|
:disabled="!isDirty || isLoading"
|
||||||
|
:loading="isSaving"
|
||||||
|
variant="flat"
|
||||||
|
@click="requestSaveConfirmation"
|
||||||
|
>
|
||||||
儲存
|
儲存
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn v-else color="primary" variant="flat" @click="requestCloseDialog">關閉</v-btn>
|
<v-btn v-else color="primary" variant="flat" @click="requestCloseDialog">關閉</v-btn>
|
||||||
@@ -190,8 +370,6 @@ v-if="!isViewMode" color="primary" :disabled="!isDirty || isLoading" :loading="i
|
|||||||
</v-overlay>
|
</v-overlay>
|
||||||
</teleport>
|
</teleport>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<maintenance-crud-dialogs
|
<maintenance-crud-dialogs
|
||||||
:close-visible="confirmCloseVisible"
|
:close-visible="confirmCloseVisible"
|
||||||
:delete-visible="confirmDeleteVisible"
|
:delete-visible="confirmDeleteVisible"
|
||||||
@@ -247,8 +425,20 @@ const { smAndUp } = useDisplay()
|
|||||||
|
|
||||||
// 表格欄位設定(含固定欄與排序)
|
// 表格欄位設定(含固定欄與排序)
|
||||||
const tableHeaders = computed(() => [
|
const tableHeaders = computed(() => [
|
||||||
{ title: '學號', key: 'studentId', sortable: true, fixed: smAndUp.value && 'start' as const, width: 120 },
|
{
|
||||||
{ title: '姓名', key: 'name', sortable: true, fixed: smAndUp.value && 'start' as const, width: 100 },
|
title: '學號',
|
||||||
|
key: 'studentId',
|
||||||
|
sortable: true,
|
||||||
|
fixed: smAndUp.value && ('start' as const),
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '姓名',
|
||||||
|
key: 'name',
|
||||||
|
sortable: true,
|
||||||
|
fixed: smAndUp.value && ('start' as const),
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
{ title: '系所', key: 'department', sortable: true, width: 140 },
|
{ title: '系所', key: 'department', sortable: true, width: 140 },
|
||||||
{ title: '年級', key: 'grade', sortable: true, width: 90 },
|
{ title: '年級', key: 'grade', sortable: true, width: 90 },
|
||||||
{ title: '入學年度', key: 'enrollYear', sortable: true, width: 110 },
|
{ title: '入學年度', key: 'enrollYear', sortable: true, width: 110 },
|
||||||
@@ -257,7 +447,14 @@ const tableHeaders = computed(() => [
|
|||||||
{ title: '電話', key: 'phone', sortable: true, width: 140 },
|
{ title: '電話', key: 'phone', sortable: true, width: 140 },
|
||||||
{ title: '指導老師', key: 'advisor', sortable: true, width: 110 },
|
{ title: '指導老師', key: 'advisor', sortable: true, width: 110 },
|
||||||
{ title: '狀態', key: 'status', sortable: true, width: 90 },
|
{ title: '狀態', key: 'status', sortable: true, width: 90 },
|
||||||
{ title: '操作', key: 'actions', sortable: false, fixed: smAndUp.value && 'end' as const, width: 'auto', cellProps: { class: 'px-0 bg-background' } },
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'actions',
|
||||||
|
sortable: false,
|
||||||
|
fixed: smAndUp.value && ('end' as const),
|
||||||
|
width: 'auto',
|
||||||
|
cellProps: { class: 'px-0 bg-background' },
|
||||||
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
// 查詢條件(示意用,未接 API)
|
// 查詢條件(示意用,未接 API)
|
||||||
@@ -372,7 +569,7 @@ const {
|
|||||||
const isFormReadonly = computed(() => isViewMode.value)
|
const isFormReadonly = computed(() => isViewMode.value)
|
||||||
|
|
||||||
// 重設查詢條件
|
// 重設查詢條件
|
||||||
function resetSearch () {
|
function resetSearch() {
|
||||||
search.value = {
|
search.value = {
|
||||||
studentId: '',
|
studentId: '',
|
||||||
name: '',
|
name: '',
|
||||||
@@ -383,7 +580,7 @@ function resetSearch () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 新增:開啟彈窗,使用預設值
|
// 新增:開啟彈窗,使用預設值
|
||||||
function openAddDialog () {
|
function openAddDialog() {
|
||||||
loadSequence.value += 1
|
loadSequence.value += 1
|
||||||
dialogMode.value = 'create'
|
dialogMode.value = 'create'
|
||||||
editingId.value = null
|
editingId.value = null
|
||||||
@@ -393,7 +590,7 @@ function openAddDialog () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 編輯:先開彈窗,資料載入後填入
|
// 編輯:先開彈窗,資料載入後填入
|
||||||
function openEditDialog (student: StudentRecord) {
|
function openEditDialog(student: StudentRecord) {
|
||||||
loadSequence.value += 1
|
loadSequence.value += 1
|
||||||
const sequence = loadSequence.value
|
const sequence = loadSequence.value
|
||||||
dialogMode.value = 'edit'
|
dialogMode.value = 'edit'
|
||||||
@@ -421,7 +618,7 @@ function openEditDialog (student: StudentRecord) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 檢視:只讀模式並預設展開所有分組
|
// 檢視:只讀模式並預設展開所有分組
|
||||||
function openViewDialog (student: StudentRecord) {
|
function openViewDialog(student: StudentRecord) {
|
||||||
loadSequence.value += 1
|
loadSequence.value += 1
|
||||||
const sequence = loadSequence.value
|
const sequence = loadSequence.value
|
||||||
dialogMode.value = 'view'
|
dialogMode.value = 'view'
|
||||||
@@ -449,7 +646,7 @@ function openViewDialog (student: StudentRecord) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 先檢核再提示儲存確認
|
// 先檢核再提示儲存確認
|
||||||
async function requestSaveConfirmation () {
|
async function requestSaveConfirmation() {
|
||||||
if (isSaving.value || isLoading.value || !isDirty.value || isViewMode.value) return
|
if (isSaving.value || isLoading.value || !isDirty.value || isViewMode.value) return
|
||||||
clearAllErrors()
|
clearAllErrors()
|
||||||
|
|
||||||
@@ -468,13 +665,13 @@ async function requestSaveConfirmation () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 儲存確認後才真正送出
|
// 儲存確認後才真正送出
|
||||||
function confirmSave () {
|
function confirmSave() {
|
||||||
confirmSaveVisible.value = false
|
confirmSaveVisible.value = false
|
||||||
saveStudent()
|
saveStudent()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 寫入資料(Demo:直接更新列表)
|
// 寫入資料(Demo:直接更新列表)
|
||||||
async function saveStudent () {
|
async function saveStudent() {
|
||||||
if (isSaving.value || isLoading.value) return
|
if (isSaving.value || isLoading.value) return
|
||||||
isSaving.value = true
|
isSaving.value = true
|
||||||
await new Promise((resolve) => setTimeout(resolve, 450))
|
await new Promise((resolve) => setTimeout(resolve, 450))
|
||||||
@@ -508,12 +705,11 @@ async function saveStudent () {
|
|||||||
}, 1600)
|
}, 1600)
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToField (field: string) {
|
function scrollToField(field: string) {
|
||||||
const target = document.getElementById(`field-${field}`)
|
const target = document.getElementById(`field-${field}`)
|
||||||
if (!target) return
|
if (!target) return
|
||||||
target.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
target.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
Reference in New Issue
Block a user