refactor: update icon usage to use mdi imports for consistency
This commit is contained in:
@@ -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 }),
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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('')
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user