refactor: update icon usage to use mdi imports for consistency

This commit is contained in:
skytek_xinliang
2026-03-26 11:48:05 +08:00
parent 069141794e
commit ec3fbace1a
50 changed files with 454 additions and 198 deletions
+2 -1
View File
@@ -48,6 +48,7 @@
</template>
<script setup lang="ts">
import { mdiSchool } from '@mdi/js'
import { ref } from 'vue'
import AnalysisBarChart from './base/analysis/AnalysisBarChart.vue'
import AnalysisDonutChart from './base/analysis/AnalysisDonutChart.vue'
@@ -102,7 +103,7 @@ const props = defineProps({
},
pie2Data: {
type: Object as () => DonutData,
default: () => ({ value: 65, label: '及格率', color: 'success', icon: 'mdi-school' }),
default: () => ({ value: 65, label: '及格率', color: 'success', icon: mdiSchool }),
},
})
+2 -1
View File
@@ -31,7 +31,7 @@ class="board-wrapper pa-2 pa-lg-0" color="rgba(var(--v-theme-surface), 0.8)" ele
<template #prepend>
<v-slide-x-transition appear>
<div class="mobile-banner-icon-wrap d-flex align-center">
<v-icon class="mobile-banner-icon" color="primary" size="small">mdi-bullhorn-variant-outline</v-icon>
<v-icon class="mobile-banner-icon" color="primary" size="small" :icon="mdiBullhornVariantOutline" />
</div>
</v-slide-x-transition>
</template>
@@ -144,6 +144,7 @@ class="d-none d-md-block" :welcome-description="props.header.welcomeDescription"
</template>
<script setup lang="ts">
import { mdiBullhornVariantOutline } from '@mdi/js'
import { computed, ref } from 'vue'
import LoginAnnouncementBoard from './base/login/LoginAnnouncementBoard.vue'
import LoginBrand from './base/login/LoginBrand.vue'
+1 -1
View File
@@ -38,7 +38,7 @@
<!-- Icon Column -->
<template #[`item.icon`]="{ item }">
<v-icon v-if="item.icon" size="small">{{ item.icon }}</v-icon>
<v-icon v-if="item.icon" size="small" :icon="item.icon" />
</template>
<!-- Permission Column -->
+2 -1
View File
@@ -57,7 +57,7 @@
@click="expand = !expand"
>
{{ expand ? collapseBtnText : expandBtnText }}
<v-icon end :icon="expand ? 'mdi-chevron-up' : 'mdi-chevron-down'"></v-icon>
<v-icon end :icon="expand ? mdiChevronUp : mdiChevronDown"></v-icon>
</v-btn>
</v-col>
</v-row>
@@ -66,6 +66,7 @@
</template>
<script setup lang="ts">
import { mdiChevronDown, mdiChevronUp } from '@mdi/js'
import { computed, reactive, ref } from 'vue'
import SKDatePicker from './input_field/SKDatePicker.vue'
import SKSelectField from './input_field/SKSelectField.vue'
+5 -4
View File
@@ -7,7 +7,7 @@
v-if="showCreate"
class="mr-4"
color="primary"
prepend-icon="mdi-plus"
:prepend-icon="mdiPlus"
@click="$emit('create')"
>
{{ createBtnText }}
@@ -23,7 +23,7 @@
variant="text"
@click="$emit('toggle-search')"
>
<v-icon :color="searchVisible ? 'primary-variant' : undefined"> mdi-magnify </v-icon>
<v-icon :color="searchVisible ? 'primary-variant' : undefined" :icon="mdiMagnify" />
</v-btn>
</template>
</v-tooltip>
@@ -31,7 +31,7 @@
<v-tooltip :disabled="!refreshTooltipText" location="top" :text="refreshTooltipText">
<template #activator="{ props }">
<v-btn v-bind="props" density="comfortable" icon variant="text" @click="$emit('refresh')">
<v-icon>mdi-refresh</v-icon>
<v-icon :icon="mdiRefresh" />
</v-btn>
</template>
</v-tooltip>
@@ -47,7 +47,7 @@
variant="text"
@click="$emit('settings')"
>
<v-icon>mdi-cog</v-icon>
<v-icon :icon="mdiCog" />
</v-btn>
</template>
</v-tooltip>
@@ -84,6 +84,7 @@
</template>
<script setup lang="ts">
import { mdiCog, mdiMagnify, mdiPlus, mdiRefresh } from '@mdi/js'
import { computed, toRefs } from 'vue'
interface SettingsItem {
+2 -1
View File
@@ -22,7 +22,7 @@
variant="text"
@click="toggleExpand(item.id)"
>
<v-icon>{{ isExpanded(item.id) ? 'mdi-chevron-down' : 'mdi-chevron-right' }}</v-icon>
<v-icon :icon="isExpanded(item.id) ? mdiChevronDown : mdiChevronRight" />
</v-btn>
<div v-else style="width: 20px"></div>
@@ -39,6 +39,7 @@
</template>
<script setup lang="ts">
import { mdiChevronDown, mdiChevronRight } from '@mdi/js'
import { computed, ref, watch } from 'vue'
interface Props {
@@ -16,7 +16,7 @@
:size="180"
:width="25"
>
<v-icon :color="data.color" size="40">{{ data.icon }}</v-icon>
<v-icon :color="data.color" size="40" :icon="data.icon" />
</v-progress-circular>
</div>
<div class="mt-6 text-center">
@@ -8,9 +8,7 @@
</div>
<div class="text-h4 font-weight-bold">{{ value }}</div>
</div>
<v-icon class="opacity-80" :color="color" size="x-large">
{{ icon }}
</v-icon>
<v-icon class="opacity-80" :color="color" size="x-large" :icon="icon" />
</div>
<div class="d-flex justify-space-between align-center border-t pt-3">
@@ -2,7 +2,7 @@
<v-card class="rounded-lg" elevation="2" v-bind="$attrs">
<v-card-title class="d-flex align-center py-4 px-4">
<div class="d-flex align-center">
<v-icon class="mr-2" color="primary" icon="mdi-chart-timeline-variant"></v-icon>
<v-icon class="mr-2" color="primary" :icon="mdiChartTimelineVariant"></v-icon>
<span>{{ title }}</span>
</div>
<v-spacer></v-spacer>
@@ -49,6 +49,7 @@
</template>
<script setup lang="ts">
import { mdiChartTimelineVariant } from '@mdi/js'
interface Props {
title: string
data: number[]
@@ -18,7 +18,7 @@
>
<div class="pa-4 h-100 hover-bg" @click="$emit('app-click', app)">
<div class="d-flex align-center mb-3">
<v-icon class="mr-3" :color="app.color" size="large">{{ app.icon }}</v-icon>
<v-icon class="mr-3" :color="app.color" size="large" :icon="app.icon" />
<span class="text-subtitle-1 font-weight-medium">{{ app.name }}</span>
</div>
<div
@@ -23,11 +23,11 @@
</div>
<div class="mt-4 d-flex justify-center gap-4 w-100">
<div class="d-flex align-center mr-4">
<v-icon class="mr-1" color="primary" size="small">mdi-circle</v-icon>
<v-icon class="mr-1" color="primary" size="small" :icon="mdiCircle" />
<span class="text-caption">{{ primaryLabel }}</span>
</div>
<div class="d-flex align-center">
<v-icon class="mr-1" color="grey-lighten-3" size="small">mdi-circle</v-icon>
<v-icon class="mr-1" color="grey-lighten-3" size="small" :icon="mdiCircle" />
<span class="text-caption">{{ secondaryLabel }}</span>
</div>
</div>
@@ -36,6 +36,7 @@
</template>
<script setup lang="ts">
import { mdiCircle } from '@mdi/js'
interface Props {
title: string
value: number
@@ -13,7 +13,7 @@
variant="text"
@click="$emit('nav-click', nav)"
>
<v-icon size="24">{{ nav.icon }}</v-icon>
<v-icon size="24" :icon="nav.icon" />
</v-btn>
<div class="text-caption text-grey-darken-1">{{ nav.title }}</div>
</v-col>
+2 -1
View File
@@ -5,7 +5,7 @@ v-model="username" bg-color="surface" class="mb-6 mb-md-4" color="primary"
density="comfortable" hide-details :placeholder="props.accPlaceholder" variant="outlined"></v-text-field>
<v-text-field
v-model="password" :append-inner-icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'" bg-color="surface"
v-model="password" :append-inner-icon="showPassword ? mdiEyeOff : mdiEye" bg-color="surface"
class="mb-6 mb-md-4" color="primary" density="comfortable" hide-details :placeholder="props.passwPlaceholder" :type="showPassword ? 'text' : 'password'"
variant="outlined"
@click:append-inner="showPassword = !showPassword"></v-text-field>
@@ -30,6 +30,7 @@ class="text-body-2 text-primary text-decoration-none" :href="props.forgotPasswor
</template>
<script setup lang="ts">
import { mdiEye, mdiEyeOff } from '@mdi/js'
import { onMounted, ref, watch } from 'vue'
const username = ref('')
const password = ref('')
+5 -4
View File
@@ -1,13 +1,13 @@
<template>
<div class="d-flex justify-end py-0 py-sm-2">
<v-btn
class="d-none d-md-block" color="grey-darken-1" icon="mdi-palette-outline" size="small" variant="text"
class="d-none d-md-block" color="grey-darken-1" :icon="mdiPaletteOutline" size="small" variant="text"
@click="toggleTheme"></v-btn>
<!-- <v-btn icon="mdi-dock-window" variant="text" size="small" color="grey-darken-1" @click="handleToggleLayout"></v-btn> -->
<!-- <v-btn :icon="mdiDockWindow" variant="text" size="small" color="grey-darken-1" @click="handleToggleLayout"></v-btn> -->
<v-menu location="bottom end">
<template #activator="{ props: menuActivatorProps }">
<v-btn
v-bind="menuActivatorProps" color="grey-darken-1" icon="mdi-translate" size="small"
v-bind="menuActivatorProps" color="grey-darken-1" :icon="mdiTranslate" size="small"
variant="text"></v-btn>
</template>
<v-list density="compact">
@@ -18,11 +18,12 @@ v-for="locale in localeOptions" :key="locale" :active="locale === props.locale"
</v-list-item>
</v-list>
</v-menu>
<!-- <v-btn icon="mdi-weather-night" variant="text" size="small" color="grey-darken-1"></v-btn> -->
<!-- <v-btn :icon="mdiWeatherNight" variant="text" size="small" color="grey-darken-1"></v-btn> -->
</div>
</template>
<script setup lang="ts">
import { mdiDockWindow, mdiPaletteOutline, mdiTranslate, mdiWeatherNight } from '@mdi/js'
import { computed } from 'vue'
import { useTheme } from 'vuetify'
import { getNextThemeName } from '@/utils/theme'
+4 -3
View File
@@ -10,16 +10,16 @@
class="captcha-wrapper d-flex align-center cursor-pointer me-1 me-md-2" :title="props.refreshTitle"
@click="handleRefresh">
<img v-if="captchaImage" alt="Captcha" class="captcha-img" :src="captchaImage" />
<v-icon class="ms-2" color="grey" icon="mdi-refresh"></v-icon>
<v-icon class="ms-2" color="grey" :icon="mdiRefresh"></v-icon>
</div>
<!-- Input and Verify -->
<v-text-field
v-model="inputCode" :append-inner-icon="props.verified ? 'mdi-check-circle' : ''" bg-color="surface" class="flex-grow-1"
v-model="inputCode" :append-inner-icon="props.verified ? mdiCheckCircle : undefined" bg-color="surface" class="flex-grow-1"
color="primary" density="compact" :disabled="props.verified" :error="!!errorMsg" hide-details
:placeholder="props.captchaPlaceholder" variant="outlined">
<template v-if="props.verified" #append-inner>
<v-icon color="success">mdi-check-circle</v-icon>
<v-icon color="success" :icon="mdiCheckCircle" />
</template>
</v-text-field>
</div>
@@ -31,6 +31,7 @@ v-model="inputCode" :append-inner-icon="props.verified ? 'mdi-check-circle' : ''
</template>
<script setup lang="ts">
import { mdiCheckCircle, mdiRefresh } from '@mdi/js'
import { computed } from 'vue'
interface CaptchaPayload {
+7 -6
View File
@@ -8,7 +8,7 @@ v-model="drawer" class="sk-admin-drawer" color="surface" :rail="isRail"
<!-- Sidebar Title -->
<v-card class="sidebar-header d-flex align-center pa-0 pl-3 py-2" flat>
<v-btn
:aria-label="sidebarToggleLabel" color="grey" :icon="isRail ? 'mdi-menu-open' : 'mdi-menu'" size="32"
:aria-label="sidebarToggleLabel" color="grey" :icon="isRail ? mdiMenuOpen : mdiMenu" size="32"
variant="text" @click="toggleSidebar" />
<v-card-text v-if="!isRail" class="sidebar-title flex-grow-1 py-0">
<slot name="title">
@@ -120,12 +120,12 @@ v-else :mobile-current-items="mobileCurrentItems"
<v-card v-if="helpWidgetVisible" class="help-widget" rounded="lg">
<v-card-item class="py-2">
<template #prepend>
<v-icon color="primary">mdi-help-circle-outline</v-icon>
<v-icon color="primary" :icon="mdiHelpCircleOutline" />
</template>
<v-card-title class="text-subtitle-2">操作說明</v-card-title>
<template #append>
<v-btn
aria-label="關閉說明" icon="mdi-close" size="small" variant="text"
aria-label="關閉說明" :icon="mdiClose" size="small" variant="text"
@click="helpWidgetVisible = false" />
</template>
</v-card-item>
@@ -141,11 +141,12 @@ aria-label="關閉說明" icon="mdi-close" size="small" variant="text"
</v-card>
</v-slide-y-reverse-transition>
<!-- <v-btn v-if="isMobile" class="mobile-menu-btn" color="primary" icon="mdi-menu" @click="drawer = true" /> -->
<!-- <v-btn v-if="isMobile" class="mobile-menu-btn" color="primary" :icon="mdiMenu" @click="drawer = true" /> -->
</v-app>
</template>
<script setup>
import { mdiClose, mdiHelpCircleOutline, mdiHome, mdiMenu, mdiMenuOpen } from '@mdi/js'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useDisplay, useTheme } from 'vuetify'
import { getNextThemeName } from '@/utils/theme'
@@ -182,7 +183,7 @@ const defaultFeatures = {
const defaultBreadcrumbConfig = {
homeLabel: '首頁',
homeDisabled: true,
homeIcon: 'mdi-home',
homeIcon: mdiHome,
}
const props = defineProps({
@@ -246,7 +247,7 @@ const props = defineProps({
default: () => ({
homeLabel: '首頁',
homeDisabled: true,
homeIcon: 'mdi-home',
homeIcon: mdiHome,
}),
},
breadcrumbItems: {
+5 -4
View File
@@ -8,7 +8,7 @@
<div class="d-flex align-center w-100">
<span class="flex-grow-1">{{ favoriteHeaderLabel }}</span>
<v-btn
density="compact" icon="mdi-unfold-less-horizontal" :ripple="false" variant="text"
density="compact" :icon="mdiUnfoldLessHorizontal" :ripple="false" variant="text"
@click.stop="collapseFavoriteGroups" />
</div>
</v-list-subheader>
@@ -55,7 +55,7 @@ density="compact" icon="mdi-unfold-less-horizontal" :ripple="false" variant="tex
<div class="d-flex align-center w-100">
<span class="flex-grow-1">{{ menuHeaderLabel }}</span>
<v-btn
density="compact" icon="mdi-unfold-less-horizontal" :ripple="false" variant="text"
density="compact" :icon="mdiUnfoldLessHorizontal" :ripple="false" variant="text"
@click.stop="collapseMenuGroups" />
</div>
</v-list-subheader>
@@ -138,13 +138,13 @@ v-for="subSubItem in subItem.subItems" :key="subSubItem.path ?? subSubItem.title
<v-spacer />
<v-tooltip location="bottom" :text="themeToggleLabel">
<template #activator="{ props }">
<v-btn v-bind="props" :aria-label="themeToggleLabel" icon="mdi-palette" variant="text" @click="toggleTheme" />
<v-btn v-bind="props" :aria-label="themeToggleLabel" :icon="mdiPalette" variant="text" @click="toggleTheme" />
</template>
</v-tooltip>
<v-tooltip location="bottom" :text="logoutLabel">
<template #activator="{ props }">
<v-btn v-bind="props" :aria-label="logoutLabel" icon="mdi-logout" variant="text" @click="$emit('logout')" />
<v-btn v-bind="props" :aria-label="logoutLabel" :icon="mdiLogout" variant="text" @click="$emit('logout')" />
</template>
</v-tooltip>
</v-app-bar>
@@ -158,6 +158,7 @@ v-for="subSubItem in subItem.subItems" :key="subSubItem.path ?? subSubItem.title
</template>
<script setup>
import { mdiLogout, mdiPalette, mdiUnfoldLessHorizontal } from '@mdi/js'
import { computed, ref } from 'vue'
import { useTheme } from 'vuetify'
import { getNextThemeName } from '@/utils/theme'
+3 -2
View File
@@ -13,7 +13,7 @@
:width="menuWidth"
>
<template #activator="{ props }">
<v-btn v-bind="props" icon="mdi-dots-vertical" />
<v-btn v-bind="props" :icon="mdiDotsVertical" />
</template>
<v-list :density="menuDensity">
@@ -51,7 +51,7 @@
<template #activator="{ props }">
<v-list-item
v-bind="props"
append-icon="mdi-chevron-right"
:append-icon="mdiChevronRight"
:prepend-icon="item.icon"
:title="item.title"
/>
@@ -88,6 +88,7 @@
</template>
<script setup>
import { mdiChevronRight, mdiDotsVertical } from '@mdi/js'
import { computed, ref } from 'vue'
import { useDisplay } from 'vuetify'
@@ -12,12 +12,12 @@ v-if="features.showFavorites && !showFavoritesBar" class="mr-2" color="primary"
</template>
<template #item="{ item }">
<div class="d-flex align-center ga-1">
<v-icon v-if="item.icon" class="mr-1" size="14">{{ item.icon }}</v-icon>
<v-icon v-if="item.icon" class="mr-1" size="14" :icon="item.icon" />
<span class="text-caption text-no-wrap">{{ item.title }}</span>
</div>
</template>
<template #divider>
<v-icon color="primary-variant" size="12">mdi-chevron-right</v-icon>
<v-icon color="primary-variant" size="12" :icon="mdiChevronRight" />
</template>
</v-breadcrumbs>
<div class="page-actions">
@@ -27,6 +27,7 @@ v-if="features.showFavorites && !showFavoritesBar" class="mr-2" color="primary"
</template>
<script setup>
import { mdiChevronRight } from '@mdi/js'
defineProps({
features: { type: Object, default: () => ({}) },
showBreadcrumbBar: { type: Boolean, default: true },
@@ -11,24 +11,25 @@ v-if="features.showFavorites && showFavoritesBar && !isMobile"
v-for="item in favoriteItems" :key="item.path ?? item.title" class="favorite-item" closable
color="secondary" size="small" variant="outlined" @click="emit('select', item)"
@click:close="emit('remove-favorite', item)">
<v-icon v-if="item.icon" class="me-1" size="16">{{ item.icon }}</v-icon>
<v-icon v-if="item.icon" class="me-1" size="16" :icon="item.icon" />
<span class="text-caption">{{ item.title }}</span>
</v-chip>
</transition-group>
<v-btn
v-if="favoritesConfig.showAdd" class="favorite-add" color="primary" size="small" variant="outlined"
@click="emit('add-favorite')">
<v-icon class="mr-1" size="16">mdi-plus</v-icon>
<v-icon class="mr-1" size="16" :icon="mdiPlus" />
<span class="text-caption">{{ favoritesConfig.addLabel }}</span>
</v-btn>
</div>
<v-btn color="grey" size="small" variant="text" @click="emit('toggle-favorites-bar', false)">
<v-icon>mdi-eye-off</v-icon>
<v-icon :icon="mdiEyeOff" />
</v-btn>
</v-col>
</template>
<script setup>
import { mdiEyeOff, mdiPlus } from '@mdi/js'
defineProps({
features: { type: Object, default: () => ({}) },
showFavoritesBar: { type: Boolean, default: true },
@@ -1,6 +1,6 @@
<template>
<v-col class="d-flex align-center bg-surface pr-2 pl-2 pl-m-3 py-1">
<v-btn v-if="isMobile" icon="mdi-menu" size="small" variant="text" @click="emit('toggle-drawer')"></v-btn>
<v-btn v-if="isMobile" :icon="mdiMenu" size="small" variant="text" @click="emit('toggle-drawer')"></v-btn>
<div v-if="features.showSearch" class="search-input-wrapper">
<v-text-field
@@ -8,7 +8,7 @@ v-model="searchValueModel" :aria-label="searchConfig.label" class="search-input"
hide-details :placeholder="searchConfig.placeholder" variant="outlined"
@keyup.enter="triggerSearch">
<template v-if="!isMobile" #prepend-inner>
<v-icon size="small">mdi-magnify</v-icon>
<v-icon size="small" :icon="mdiMagnify" />
</template>
<template #append-inner>
<v-btn :aria-label="searchConfig.label" color="primary" size="small" variant="text" @click="triggerSearch">
@@ -30,9 +30,9 @@ v-bind="props" :aria-label="toolbarActions.notificationsLabel" icon size="small"
<v-badge
v-if="toolbarCounts.notifications" color="error" :content="toolbarCounts.notifications"
offset-x="4" offset-y="-2">
<v-icon>mdi-bell-outline</v-icon>
<v-icon :icon="mdiBellOutline" />
</v-badge>
<v-icon v-else>mdi-bell-outline</v-icon>
<v-icon v-else :icon="mdiBellOutline" />
</v-btn>
</template>
</v-tooltip>
@@ -46,9 +46,9 @@ v-bind="props" :aria-label="toolbarActions.messagesLabel" icon size="small" vari
<v-badge
v-if="toolbarCounts.messages" color="warning" :content="toolbarCounts.messages" offset-x="4"
offset-y="-2">
<v-icon>mdi-message-text-outline</v-icon>
<v-icon :icon="mdiMessageTextOutline" />
</v-badge>
<v-icon v-else>mdi-message-text-outline</v-icon>
<v-icon v-else :icon="mdiMessageTextOutline" />
</v-btn>
</template>
</v-tooltip>
@@ -59,7 +59,7 @@ v-if="toolbarCounts.messages" color="warning" :content="toolbarCounts.messages"
<v-btn
v-bind="props" :aria-label="toolbarActions.helpLabel" icon size="small" variant="text"
@click="emit('action', 'help')">
<v-icon>mdi-help</v-icon>
<v-icon :icon="mdiHelp" />
</v-btn>
</template>
</v-tooltip>
@@ -72,7 +72,7 @@ v-bind="props" :aria-label="toolbarActions.helpLabel" icon size="small" variant=
<v-btn
v-bind="{ ...menuProps, ...tooltipProps }" :aria-label="toolbarActions.settingsLabel" icon size="small"
variant="text">
<v-icon>mdi-cog-outline</v-icon>
<v-icon :icon="mdiCogOutline" />
</v-btn>
</template>
</v-tooltip>
@@ -100,7 +100,7 @@ v-bind="{ ...menuProps, ...tooltipProps }" :aria-label="toolbarActions.settingsL
<v-tooltip location="bottom" :text="logoutLabel">
<template #activator="{ props }">
<v-btn v-bind="props" :aria-label="logoutLabel" icon size="small" variant="text" @click="emit('logout')">
<v-icon>mdi-logout</v-icon>
<v-icon :icon="mdiLogout" />
</v-btn>
</template>
</v-tooltip>
@@ -108,7 +108,7 @@ v-bind="{ ...menuProps, ...tooltipProps }" :aria-label="toolbarActions.settingsL
<v-tooltip v-if="features.showThemeToggle" location="bottom" :text="themeToggleLabel">
<template #activator="{ props }">
<v-btn v-bind="props" :aria-label="themeToggleLabel" icon variant="text" @click="emit('toggle-theme')">
<v-icon>mdi-palette-outline</v-icon>
<v-icon :icon="mdiPaletteOutline" />
</v-btn>
</template>
</v-tooltip>
@@ -118,6 +118,7 @@ v-bind="{ ...menuProps, ...tooltipProps }" :aria-label="toolbarActions.settingsL
</template>
<script setup>
import { mdiBellOutline, mdiCogOutline, mdiHelp, mdiLogout, mdiMagnify, mdiMenu, mdiMessageTextOutline, mdiPaletteOutline } from '@mdi/js'
import { computed } from 'vue'
const props = defineProps({
@@ -7,7 +7,7 @@
v-bind="isShrink ? undefined : props" :class="{ 'px-0': isShrink }"
:link="isNavigable(item) && !!item.path" :to="isNavigable(item) ? item.path : undefined" @click="emitSelect(item)">
<template #prepend>
<v-icon v-if="item.icon" size="20">{{ item.icon }}</v-icon>
<v-icon v-if="item.icon" size="20" :icon="item.icon" />
<v-btn v-if="isShrink && !item.icon" class="" rounded size="36" spaced="start" variant="text">{{
item.title?.charAt(0) }}</v-btn>
</template>
@@ -31,7 +31,7 @@ v-if="subItem.subItems?.length"
<template #activator="{ props: subProps }">
<v-list-item
v-bind="subProps" :link="isNavigable(subItem)"
:prepend-icon="subItem.icon || 'mdi-menu-right'" :to="isNavigable(subItem) ? subItem.path : undefined"
:prepend-icon="subItem.icon || mdiMenuRight" :to="isNavigable(subItem) ? subItem.path : undefined"
@click="emitSelect(subItem)">
<template #title>
<span class="text-body-2 nav-text-overflow">{{ subItem.title }}</span>
@@ -48,7 +48,7 @@ v-if="getItemCount(subItem) > 0" class="menu-count" color="secondary" size="x-sm
<v-list-item
v-for="subSubItem in subItem.subItems" :key="subSubItem.path ?? subSubItem.title"
:link="!!subSubItem.path" prepend-icon="mdi-circle-small" :to="subSubItem.path"
:link="!!subSubItem.path" :prepend-icon="mdiCircleSmall" :to="subSubItem.path"
@click="emitSelect(subSubItem)">
<template #title>
<v-tooltip location="end" :text="subSubItem.title">
@@ -61,7 +61,7 @@ v-for="subSubItem in subItem.subItems" :key="subSubItem.path ?? subSubItem.title
</v-list-group>
<v-list-item
v-else :link="!!subItem.path" :prepend-icon="subItem.icon || 'mdi-menu-right'" :to="subItem.path"
v-else :link="!!subItem.path" :prepend-icon="subItem.icon || mdiMenuRight" :to="subItem.path"
@click="emitSelect(subItem)">
<template #title>
<v-tooltip location="end" :text="subItem.title">
@@ -76,7 +76,7 @@ v-else :link="!!subItem.path" :prepend-icon="subItem.icon || 'mdi-menu-right'" :
<v-list-item v-else :class="{ 'px-0': isShrink }" :link="!!item.path" :to="item.path" @click="emitSelect(item)">
<template #prepend>
<v-icon v-if="item.icon" size="20">{{ item.icon }}</v-icon>
<v-icon v-if="item.icon" size="20" :icon="item.icon" />
<v-btn v-if="isShrink && !item.icon" class="" rounded size="36" spaced="start" variant="text">{{
item.title?.charAt(0) }}</v-btn>
</template>
@@ -93,6 +93,7 @@ v-else :link="!!subItem.path" :prepend-icon="subItem.icon || 'mdi-menu-right'" :
</template>
<script setup lang="ts">
import { mdiCircleSmall, mdiMenuRight } from '@mdi/js'
import { computed, watch } from 'vue'
interface MenuItem {
@@ -6,7 +6,7 @@ v-for="item in mobileCurrentItems" :key="item.path ?? item.title" class="mb-1" r
@click="emit('item-click', item)">
<v-list-item-title class="text-body-2">{{ item.title }}</v-list-item-title>
<template #append>
<v-icon size="18">{{ item.subItems?.length ? 'mdi-chevron-right' : 'mdi-arrow-top-right' }}</v-icon>
<v-icon size="18" :icon="item.subItems?.length ? mdiChevronRight : mdiArrowTopRight" />
</template>
</v-list-item>
</v-list>
@@ -14,6 +14,7 @@ v-for="item in mobileCurrentItems" :key="item.path ?? item.title" class="mb-1" r
</template>
<script setup>
import { mdiArrowTopRight, mdiChevronRight } from '@mdi/js'
defineProps({
mobileCurrentItems: {
type: Array,
+4 -3
View File
@@ -5,11 +5,11 @@
<span class="text-h6">{{ title }}</span>
<v-spacer />
<v-btn
:icon="mdAndUp ? false : 'mdi-magnify'" :prepend-icon="mdAndUp ? 'mdi-magnify' : undefined" size="small"
:icon="mdAndUp ? false : mdiMagnify" :prepend-icon="mdAndUp ? mdiMagnify : undefined" size="small"
:text="mdAndUp ? '搜尋條件' : false" variant="text" @click="$emit('toggle-search')">
</v-btn>
<v-btn
color="primary" :icon="mdAndUp ? false : 'mdi-plus'" :prepend-icon="mdAndUp ? 'mdi-plus' : undefined"
color="primary" :icon="mdAndUp ? false : mdiPlus" :prepend-icon="mdAndUp ? mdiPlus : undefined"
size="small" :text="mdAndUp ? createLabel : false" @click="$emit('create')">
</v-btn>
</v-card-title>
@@ -34,7 +34,7 @@ color="primary" :icon="mdAndUp ? false : 'mdi-plus'" :prepend-icon="mdAndUp ? 'm
<v-card-title class="d-flex align-center py-3 px-4">
<span class="text-subtitle-1 font-weight-medium">搜尋條件</span>
<v-spacer />
<v-btn icon="mdi-close" size="small" variant="text" @click="$emit('toggle-search')" />
<v-btn :icon="mdiClose" size="small" variant="text" @click="$emit('toggle-search')" />
</v-card-title>
<v-divider />
<v-card-text class="px-2 py-1">
@@ -47,6 +47,7 @@ color="primary" :icon="mdAndUp ? false : 'mdi-plus'" :prepend-icon="mdAndUp ? 'm
</template>
<script setup lang="ts">
import { mdiClose, mdiMagnify, mdiPlus } from '@mdi/js'
import { useDisplay } from 'vuetify'
const { mdAndUp } = useDisplay()
@@ -2,20 +2,20 @@
<div v-if="mobile" class="d-flex align-center flex-wrap ga-2 w-100">
<div class="d-flex align-center ga-1">
<v-btn
v-if="isViewMode || isEditMode" :disabled="!hasPrevRecord" icon="mdi-chevron-left" size="small" variant="text"
v-if="isViewMode || isEditMode" :disabled="!hasPrevRecord" :icon="mdiChevronLeft" size="small" variant="text"
@click="$emit('prev')" />
<v-btn
v-if="isViewMode || isEditMode" :disabled="!hasNextRecord" icon="mdi-chevron-right" size="small" variant="text"
v-if="isViewMode || isEditMode" :disabled="!hasNextRecord" :icon="mdiChevronRight" size="small" variant="text"
@click="$emit('next')" />
</div>
<v-spacer />
<v-btn
v-if="isViewMode" color="primary" prepend-icon="mdi-pencil" size="small" variant="tonal"
v-if="isViewMode" color="primary" :prepend-icon="mdiPencil" size="small" variant="tonal"
@click="$emit('switch-to-edit')">
{{ editLabel }}
</v-btn>
<v-btn
v-if="isEditMode" color="primary" prepend-icon="mdi-eye" size="small" variant="tonal"
v-if="isEditMode" color="primary" :prepend-icon="mdiEye" size="small" variant="tonal"
@click="$emit('switch-to-view')">
{{ viewLabel }}
</v-btn>
@@ -23,33 +23,33 @@ v-if="isEditMode" color="primary" prepend-icon="mdi-eye" size="small" variant="t
<template v-else>
<v-btn
v-if="isViewMode || isEditMode" :disabled="!hasPrevRecord" prepend-icon="mdi-skip-previous" size="small"
v-if="isViewMode || isEditMode" :disabled="!hasPrevRecord" :prepend-icon="mdiSkipPrevious" size="small"
variant="text" @click="$emit('first')">
{{ firstLabel }}
</v-btn>
<v-btn
v-if="isViewMode || isEditMode" :disabled="!hasPrevRecord" prepend-icon="mdi-chevron-left" size="small"
v-if="isViewMode || isEditMode" :disabled="!hasPrevRecord" :prepend-icon="mdiChevronLeft" size="small"
variant="text" @click="$emit('prev')">
{{ prevLabel }}
</v-btn>
<v-btn
v-if="isViewMode || isEditMode" append-icon="mdi-chevron-right" :disabled="!hasNextRecord" size="small"
v-if="isViewMode || isEditMode" :append-icon="mdiChevronRight" :disabled="!hasNextRecord" size="small"
variant="text" @click="$emit('next')">
{{ nextLabel }}
</v-btn>
<v-btn
v-if="isViewMode || isEditMode" append-icon="mdi-skip-next" :disabled="!hasNextRecord" size="small"
v-if="isViewMode || isEditMode" :append-icon="mdiSkipNext" :disabled="!hasNextRecord" size="small"
variant="text" @click="$emit('last')">
{{ lastLabel }}
</v-btn>
<v-spacer />
<v-btn
v-if="isViewMode" color="primary" prepend-icon="mdi-pencil" size="small" variant="tonal"
v-if="isViewMode" color="primary" :prepend-icon="mdiPencil" size="small" variant="tonal"
@click="$emit('switch-to-edit')">
{{ editLabel }}
</v-btn>
<v-btn
v-if="isEditMode" color="primary" prepend-icon="mdi-eye" size="small" variant="tonal"
v-if="isEditMode" color="primary" :prepend-icon="mdiEye" size="small" variant="tonal"
@click="$emit('switch-to-view')">
{{ viewLabel }}
</v-btn>
@@ -57,6 +57,7 @@ v-if="isEditMode" color="primary" prepend-icon="mdi-eye" size="small" variant="t
</template>
<script setup lang="ts">
import { mdiChevronLeft, mdiChevronRight, mdiEye, mdiPencil, mdiSkipNext, mdiSkipPrevious } from '@mdi/js'
defineProps({
isViewMode: {
type: Boolean,
@@ -1,7 +1,7 @@
<template>
<v-card border class="d-flex flex-column h-100 rounded-0" flat width="100%">
<v-toolbar color="transparent" density="compact" flat>
<v-btn icon="mdi-arrow-left" size="small" variant="text" @click="$emit('close')" />
<v-btn :icon="mdiArrowLeft" size="small" variant="text" @click="$emit('close')" />
<v-toolbar-title class="text-subtitle-1 font-weight-bold">
{{ semester?.semesterName || '學期明細' }}
</v-toolbar-title>
@@ -84,7 +84,7 @@
<v-btn
color="primary"
:disabled="isFormLocked"
prepend-icon="mdi-plus"
:prepend-icon="mdiPlus"
size="small"
variant="text"
@click="$emit('add-course', semester.id)"
@@ -100,7 +100,7 @@
<v-btn
color="error"
:disabled="isFormLocked"
icon="mdi-delete"
:icon="mdiDelete"
size="small"
variant="text"
@click="$emit('delete-course', semester.id, idx, course.name)"
@@ -159,6 +159,7 @@
</template>
<script setup lang="ts">
import { mdiArrowLeft, mdiDelete, mdiPlus } from '@mdi/js'
import type { CourseRecord, SemesterRecord } from '@/stores/semesters'
import { computed } from 'vue'
@@ -1,6 +1,6 @@
<template>
<div class="text-subtitle-1 font-weight-bold my-3 d-flex align-center">
<v-icon start>mdi-school</v-icon>
<v-icon start :icon="mdiSchool" />
子檔資料示範
</div>
@@ -20,7 +20,7 @@
<div class="text-body-1 font-weight-bold">{{ semester.semesterName }}</div>
<div class="text-caption text-medium-emphasis">點擊查看課程與成績</div>
</div>
<v-icon size="small">mdi-chevron-right</v-icon>
<v-icon size="small" :icon="mdiChevronRight" />
</div>
<div class="d-flex flex-wrap ga-2 mt-3">
@@ -49,7 +49,7 @@
v-if="!isFormReadonly"
color="primary"
:disabled="isFormLocked"
prepend-icon="mdi-plus"
:prepend-icon="mdiPlus"
size="small"
variant="tonal"
@click="$emit('add-course', semester.id)"
@@ -64,19 +64,19 @@
<th class="cursor-pointer" width="50%" @click="toggleSort(semester.id, 'name')">
<div class="d-flex align-center ga-1">
<span>課程名稱</span>
<v-icon size="small">{{ getSortIcon(semester.id, 'name') }}</v-icon>
<v-icon size="small" :icon="getSortIcon(semester.id, 'name')" />
</div>
</th>
<th class="cursor-pointer" @click="toggleSort(semester.id, 'credits')">
<div class="d-flex align-center ga-1">
<span>學分</span>
<v-icon size="small">{{ getSortIcon(semester.id, 'credits') }}</v-icon>
<v-icon size="small" :icon="getSortIcon(semester.id, 'credits')" />
</div>
</th>
<th class="cursor-pointer" @click="toggleSort(semester.id, 'score')">
<div class="d-flex align-center ga-1">
<span>分數</span>
<v-icon size="small">{{ getSortIcon(semester.id, 'score') }}</v-icon>
<v-icon size="small" :icon="getSortIcon(semester.id, 'score')" />
</div>
</th>
<th v-if="!isFormReadonly" width="52"></th>
@@ -130,7 +130,7 @@
<v-btn
color="error"
:disabled="isFormLocked"
icon="mdi-delete-outline"
:icon="mdiDeleteOutline"
size="small"
variant="text"
@click="$emit('delete-course', semester.id, originalIndex, course.name)"
@@ -143,7 +143,7 @@
:colspan="isFormReadonly ? 3 : 4"
>
<div class="d-flex flex-column align-center ga-2">
<v-icon color="medium-emphasis" size="24">mdi-book-open-outline</v-icon>
<v-icon color="medium-emphasis" size="24" :icon="mdiBookOpenOutline" />
<span class="text-caption">尚無課程,點擊「加入課程」新增</span>
</div>
</td>
@@ -165,6 +165,7 @@
</template>
<script setup lang="ts">
import { mdiArrowDown, mdiArrowUp, mdiBookOpenOutline, mdiChevronRight, mdiDeleteOutline, mdiPlus, mdiSchool, mdiSwapVertical } from '@mdi/js'
import type { CourseRecord, SemesterRecord } from '@/stores/semesters'
import { ref } from 'vue'
@@ -222,8 +223,8 @@ function toggleSort (semesterId: number, key: CourseSortKey) {
function getSortIcon (semesterId: number, key: CourseSortKey) {
const current = getSortState(semesterId)
if (current?.key !== key) return 'mdi-swap-vertical'
return current.order === 'asc' ? 'mdi-arrow-up' : 'mdi-arrow-down'
if (current?.key !== key) return mdiSwapVertical
return current.order === 'asc' ? mdiArrowUp : mdiArrowDown
}
function compareCourseValue (left: CourseRecord, right: CourseRecord, key: CourseSortKey) {
@@ -1,7 +1,7 @@
<template>
<v-card border class="d-flex flex-column h-100 rounded-0" flat width="100%">
<v-toolbar color="transparent" density="compact" flat>
<v-btn icon="mdi-arrow-left" size="small" variant="text" @click="$emit('close')" />
<v-btn :icon="mdiArrowLeft" size="small" variant="text" @click="$emit('close')" />
<v-toolbar-title class="text-subtitle-1 font-weight-bold">
{{ semester?.semesterName || '課程明細' }}
</v-toolbar-title>
@@ -36,7 +36,7 @@
v-if="!isViewMode"
color="primary"
:disabled="isFormLocked"
prepend-icon="mdi-plus"
:prepend-icon="mdiPlus"
size="small"
variant="text"
@click="$emit('add-course', semester.id)"
@@ -69,7 +69,7 @@
<v-btn
color="error"
:disabled="isFormLocked"
icon="mdi-delete"
:icon="mdiDelete"
size="small"
variant="text"
@click="$emit('delete-course', semester.id, idx)"
@@ -129,6 +129,7 @@
</template>
<script setup lang="ts">
import { mdiArrowLeft, mdiDelete, mdiPlus } from '@mdi/js'
import type { CourseRecord, SemesterRecord } from '@/stores/semesters'
import { computed } from 'vue'
@@ -1,10 +1,10 @@
<template>
<div class="text-subtitle-1 font-weight-bold my-3 d-flex align-center">
<v-icon start>mdi-school</v-icon>
<v-icon start :icon="mdiSchool" />
子檔資料示範
<v-spacer />
<v-btn
v-if="!isMobile && !isFormReadonly && !isFormLocked" color="primary" prepend-icon="mdi-plus" size="small"
v-if="!isMobile && !isFormReadonly && !isFormLocked" color="primary" :prepend-icon="mdiPlus" size="small"
variant="tonal" @click="$emit('add-course')">
新增成績
</v-btn>
@@ -22,7 +22,7 @@ v-for="semester in semesters" :key="semester.id" class="cursor-pointer" :class="
<div class="text-body-1 font-weight-bold">{{ semester.semesterName }}</div>
<div class="text-caption text-medium-emphasis">點擊查看課程與成績</div>
</div>
<v-icon size="small">mdi-chevron-right</v-icon>
<v-icon size="small" :icon="mdiChevronRight" />
</div>
<div class="d-flex flex-wrap ga-2 mt-3">
@@ -65,7 +65,7 @@ v-else density="compact" :disabled="isFormLocked" hide-details
</template>
<template #[`item.actions`]="slotProps">
<v-btn
color="error" :disabled="isFormLocked" icon="mdi-delete" size="small" variant="text"
color="error" :disabled="isFormLocked" :icon="mdiDelete" size="small" variant="text"
@click="$emit('delete-course', slotProps.item.semesterId, slotProps.item.courseIndex)" />
</template>
</v-data-table>
@@ -80,6 +80,7 @@ color="error" :disabled="isFormLocked" icon="mdi-delete" size="small" variant="t
</template>
<script setup lang="ts">
import { mdiChevronRight, mdiDelete, mdiPlus, mdiSchool } from '@mdi/js'
import type { CourseRecord, SemesterRecord } from '@/stores/semesters'
import { computed } from 'vue'
@@ -1,9 +1,9 @@
<template>
<div class="text-subtitle-1 font-weight-bold mb-3 d-flex align-center">
<v-icon start>mdi-school</v-icon>
<v-icon start :icon="mdiSchool" />
子檔資料示範 ({{ semesters.length }})
<v-spacer />
<v-btn v-if="!isViewMode" color="primary" prepend-icon="mdi-plus" 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>
</div>
@@ -23,7 +23,7 @@
平均: {{ semester.average }} 排名: {{ semester.rank }}
</v-list-item-subtitle>
<template #append>
<v-icon size="small">mdi-chevron-right</v-icon>
<v-icon size="small" :icon="mdiChevronRight" />
</template>
</v-list-item>
</v-card>
@@ -36,6 +36,7 @@
</template>
<script setup lang="ts">
import { mdiChevronRight, mdiPlus, mdiSchool } from '@mdi/js'
import type { SemesterRecord } from '@/stores/semesters'
defineProps<{
@@ -1,7 +1,7 @@
<template>
<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-btn :icon="isMobile ? 'mdi-arrow-left' : 'mdi-close'" 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">
{{ selectedSemester ? selectedSemester.semesterName : '學期明細' }}
</v-toolbar-title>
@@ -9,7 +9,7 @@
<v-btn
v-if="selectedSemester && !isViewMode"
color="error"
prepend-icon="mdi-delete"
:prepend-icon="mdiDelete"
size="small"
variant="text"
@click="$emit('delete', selectedSemester.id)"
@@ -19,7 +19,7 @@
<v-btn
v-if="selectedSemester && !isViewMode"
color="primary"
prepend-icon="mdi-pencil"
:prepend-icon="mdiPencil"
size="small"
variant="text"
@click="$emit('start-edit')"
@@ -29,7 +29,7 @@
</v-toolbar>
<v-toolbar v-else density="compact" flat>
<v-btn :icon="isMobile ? 'mdi-arrow-left' : 'mdi-close'" size="small" variant="text" @click="$emit('cancel-edit')" />
<v-btn :icon="isMobile ? mdiArrowLeft : mdiClose" size="small" variant="text" @click="$emit('cancel-edit')" />
<v-toolbar-title class="text-subtitle-1 font-weight-bold">
編輯學期
</v-toolbar-title>
@@ -107,21 +107,23 @@
<v-row dense>
<v-col cols="12">
<v-text-field
v-model="detailForm.semesterName"
:model-value="detailForm.semesterName"
density="compact"
hide-details="auto"
label="學期名稱"
variant="outlined"
@update:model-value="updateDetailFormField('semesterName', $event)"
/>
</v-col>
<v-col :cols="isMobile ? 12 : 6">
<v-text-field
v-model.number="detailForm.rank"
:model-value="detailForm.rank"
density="compact"
hide-details="auto"
label="班級排名"
type="number"
variant="outlined"
@update:model-value="updateDetailFormField('rank', toNumber($event))"
/>
</v-col>
<v-col :cols="isMobile ? 12 : 6">
@@ -139,7 +141,7 @@
<div class="d-flex align-center mt-2 mb-1">
<div class="text-subtitle-2 font-weight-bold">課程列表</div>
<v-spacer />
<v-btn color="primary" prepend-icon="mdi-plus" size="small" variant="text" @click="$emit('add-course')">
<v-btn color="primary" :prepend-icon="mdiPlus" size="small" variant="text" @click="addCourse">
加入課程
</v-btn>
</div>
@@ -149,26 +151,42 @@
<div class="d-flex align-center mb-3">
<div class="text-subtitle-2 font-weight-bold">課程 {{ idx + 1 }}</div>
<v-spacer />
<v-btn color="error" icon="mdi-delete" size="small" variant="text" @click="$emit('remove-course', idx)" />
<v-btn color="error" :icon="mdiDelete" size="small" variant="text" @click="removeCourse(idx)" />
</div>
<div class="d-flex flex-column ga-3">
<v-text-field v-model="course.name" density="compact" hide-details label="課程名稱" variant="outlined" />
<v-text-field v-model="course.code" density="compact" hide-details label="代碼" variant="outlined" />
<v-text-field
v-model.number="course.credits"
:model-value="course.name"
density="compact"
hide-details
label="課程名稱"
variant="outlined"
@update:model-value="updateCourseField(idx, 'name', $event)"
/>
<v-text-field
:model-value="course.code"
density="compact"
hide-details
label="代碼"
variant="outlined"
@update:model-value="updateCourseField(idx, 'code', $event)"
/>
<v-text-field
:model-value="course.credits"
density="compact"
hide-details
label="學分"
type="number"
variant="outlined"
@update:model-value="updateCourseField(idx, 'credits', toNumber($event))"
/>
<v-text-field
v-model.number="course.score"
:model-value="course.score"
density="compact"
hide-details
label="分數"
type="number"
variant="outlined"
@update:model-value="updateCourseField(idx, 'score', toNumber($event))"
/>
</div>
</v-card>
@@ -189,23 +207,47 @@
<tbody>
<tr v-for="(course, idx) in detailForm.courses" :key="idx">
<td class="px-0 text-center">
<v-btn color="error" icon="mdi-delete" size="small" variant="text" @click="$emit('remove-course', idx)" />
<v-btn color="error" :icon="mdiDelete" size="small" variant="text" @click="removeCourse(idx)" />
</td>
<td class="py-2">
<v-text-field v-model="course.name" class="mb-1" density="compact" hide-details label="課程名稱" variant="underlined" />
<v-text-field v-model="course.code" density="compact" hide-details label="代碼" style="font-size: 0.85em" variant="underlined" />
<v-text-field
:model-value="course.name"
class="mb-1"
density="compact"
hide-details
label="課程名稱"
variant="underlined"
@update:model-value="updateCourseField(idx, 'name', $event)"
/>
<v-text-field
:model-value="course.code"
density="compact"
hide-details
label="代碼"
style="font-size: 0.85em"
variant="underlined"
@update:model-value="updateCourseField(idx, 'code', $event)"
/>
</td>
<td class="align-top py-2">
<v-text-field
v-model.number="course.credits"
:model-value="course.credits"
density="compact"
hide-details
type="number"
variant="outlined"
@update:model-value="updateCourseField(idx, 'credits', toNumber($event))"
/>
</td>
<td class="align-top py-2">
<v-text-field v-model.number="course.score" density="compact" hide-details type="number" variant="outlined" />
<v-text-field
:model-value="course.score"
density="compact"
hide-details
type="number"
variant="outlined"
@update:model-value="updateCourseField(idx, 'score', toNumber($event))"
/>
</td>
</tr>
<tr v-if="detailForm.courses.length === 0">
@@ -219,6 +261,7 @@
</template>
<script setup lang="ts">
import { mdiArrowLeft, mdiClose, mdiDelete, mdiPencil, mdiPlus } from '@mdi/js'
import type { SemesterRecord } from '@/stores/semesters'
import { computed } from 'vue'
@@ -231,7 +274,7 @@ const props = defineProps<{
isMobile: boolean
}>()
defineEmits<{
const emit = defineEmits<{
(event: 'close'): void
(event: 'start-edit'): void
(event: 'delete', id: number): void
@@ -239,8 +282,64 @@ defineEmits<{
(event: 'save-edit'): void
(event: 'add-course'): void
(event: 'remove-course', index: number): void
(event: 'update:detail-form', value: SemesterRecord | null): void
}>()
function cloneDetailForm (form: SemesterRecord): SemesterRecord {
return {
...form,
courses: form.courses.map((course) => ({ ...course })),
}
}
function emitDetailFormUpdate (updater: (draft: SemesterRecord) => void) {
if (!props.detailForm) return
const nextForm = cloneDetailForm(props.detailForm)
updater(nextForm)
emit('update:detail-form', nextForm)
}
function updateDetailFormField<K extends keyof SemesterRecord> (key: K, value: SemesterRecord[K]) {
emitDetailFormUpdate((draft) => {
draft[key] = value
})
}
function updateCourseField<K extends keyof SemesterRecord['courses'][number]>(
index: number,
key: K,
value: SemesterRecord['courses'][number][K],
) {
emitDetailFormUpdate((draft) => {
const course = draft.courses[index]
if (!course) return
course[key] = value
})
}
function toNumber (value: unknown): number {
if (typeof value === 'number') return value
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : 0
}
function addCourse () {
emitDetailFormUpdate((draft) => {
draft.courses.push({
code: '',
name: '',
credits: 3,
score: 0,
})
})
}
function removeCourse (index: number) {
emitDetailFormUpdate((draft) => {
draft.courses.splice(index, 1)
})
}
const totalCredits = computed(() =>
props.selectedSemester?.courses.reduce((sum, course) => sum + course.credits, 0) ?? 0,
)