refactor: update icon usage to use mdi imports for consistency
This commit is contained in:
@@ -8,13 +8,10 @@ export default vuetify({
|
||||
rules: {
|
||||
'vue/no-required-prop-with-default': 'off',
|
||||
'vue/attributes-order': 'off',
|
||||
'vue/no-template-shadow': 'off',
|
||||
'@typescript-eslint/unified-signatures': 'off',
|
||||
'@typescript-eslint/member-ordering': 'off',
|
||||
'unicorn/prefer-query-selector': 'off',
|
||||
'unicorn/no-array-sort':'off',
|
||||
"vue/no-mutating-props" : "off",
|
||||
'unicorn/prefer-logical-operator-over-ternary': 'off',
|
||||
'unicorn/prefer-structured-clone': 'off',
|
||||
}
|
||||
})
|
||||
|
||||
+24
-23
@@ -9,11 +9,11 @@
|
||||
<v-btn
|
||||
color="secondary" :disabled="isFavoriteActionDisabled" size="small" variant="outlined"
|
||||
@click="toggleFavorite">
|
||||
<v-icon class="mr-1" size="14">{{ favoriteActionIcon }}</v-icon>
|
||||
<v-icon class="mr-1" size="14" :icon="favoriteActionIcon" />
|
||||
{{ favoriteActionLabel }}
|
||||
</v-btn>
|
||||
<v-btn class="ml-2" color="primary" size="small" variant="text" @click="goHome">
|
||||
<v-icon class="mr-1" size="14">mdi-home</v-icon>
|
||||
<v-icon class="mr-1" size="14" :icon="mdiHome" />
|
||||
返回首頁
|
||||
</v-btn>
|
||||
</template>
|
||||
@@ -28,7 +28,7 @@ v-model="activeTab" bg-color="background" color="primary" density="compact" show
|
||||
<v-btn
|
||||
class="pl-2" color="grey" density="compact" :disabled="tabs.length <= 1" icon size="x-small"
|
||||
variant="text" @click.prevent.stop="closeTab(tab.path)">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
<v-icon :icon="mdiClose" />
|
||||
</v-btn>
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
@@ -58,7 +58,7 @@ class="pl-2" color="grey" density="compact" :disabled="tabs.length <= 1" icon si
|
||||
<v-list v-else density="compact">
|
||||
<v-list-item v-for="item in searchResults" :key="item.path" class="mb-2" @click="handleSearchSelect(item)">
|
||||
<template #prepend>
|
||||
<v-icon v-if="item.icon" size="18">{{ item.icon }}</v-icon>
|
||||
<v-icon v-if="item.icon" size="18" :icon="item.icon" />
|
||||
</template>
|
||||
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="item.parents?.length">
|
||||
@@ -94,7 +94,7 @@ class="pl-2" color="grey" density="compact" :disabled="tabs.length <= 1" icon si
|
||||
<v-list-item v-for="wrapped in items" :key="resolveMessageItem(wrapped).id" border="sm" class="pa-2 mb-1">
|
||||
<template #prepend>
|
||||
<v-avatar color="primary" size="28" variant="tonal">
|
||||
<v-icon size="16">{{ resolveMessageItem(wrapped).icon }}</v-icon>
|
||||
<v-icon size="16" :icon="resolveMessageItem(wrapped).icon" />
|
||||
</v-avatar>
|
||||
</template>
|
||||
<v-list-item-title class="text-body-2 font-weight-medium">
|
||||
@@ -122,6 +122,7 @@ v-model="snackbar.visible" :color="snackbar.color" :location="snackbar.location"
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { mdiAccountGroup, mdiBellOutline, mdiCalendarOutline, mdiChartBoxOutline, mdiClose, mdiCloseCircle, mdiCog, mdiDomain, mdiFileDocumentOutline, mdiFileTreeOutline, mdiHome, mdiHomeCityOutline, mdiMenu, mdiPlusCircle, mdiSchoolOutline, mdiTableEdit, mdiViewDashboardVariant } from '@mdi/js'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { SKAdminLayout, SKEmptyLayout } from '../src'
|
||||
@@ -150,11 +151,11 @@ const _fixedMenuItems = [
|
||||
title: '資料維護',
|
||||
navigable: false,
|
||||
subItems: [
|
||||
{ title: '單筆資料維護', icon: 'mdi-file-document-outline', path: '/single-record-maintenance' },
|
||||
{ title: '主從資料維護A', icon: 'mdi-file-tree-outline', path: '/master-detail-maintenance' },
|
||||
{ title: '主從資料維護B', icon: 'mdi-file-tree-outline', path: '/master-detail-maintenance-b' },
|
||||
{ title: '主從資料維護C', icon: 'mdi-file-tree-outline', path: '/master-detail-maintenance-c' },
|
||||
{ title: '可編輯表格維護', icon: 'mdi-table-edit', path: '/editable-grid-maintenance' },
|
||||
{ title: '單筆資料維護', icon: mdiFileDocumentOutline, path: '/single-record-maintenance' },
|
||||
{ title: '主從資料維護A', icon: mdiFileTreeOutline, path: '/master-detail-maintenance' },
|
||||
{ title: '主從資料維護B', icon: mdiFileTreeOutline, path: '/master-detail-maintenance-b' },
|
||||
{ title: '主從資料維護C', icon: mdiFileTreeOutline, path: '/master-detail-maintenance-c' },
|
||||
{ title: '可編輯表格維護', icon: mdiTableEdit, path: '/editable-grid-maintenance' },
|
||||
],
|
||||
},
|
||||
{ title: '登入頁', path: '/login' },
|
||||
@@ -162,18 +163,18 @@ const _fixedMenuItems = [
|
||||
|
||||
// 範例選單(用於 tab 顯示名稱的保底資料)
|
||||
const _menuItemsExample = [
|
||||
{ title: '首頁', icon: 'mdi-home', path: '/' },
|
||||
{ title: '工作台', icon: 'mdi-view-dashboard-variant', path: '/dashboard' },
|
||||
{ title: '分析頁', icon: 'mdi-chart-box-outline', path: '/analysis' },
|
||||
{ title: '首頁', icon: mdiHome, path: '/' },
|
||||
{ title: '工作台', icon: mdiViewDashboardVariant, path: '/dashboard' },
|
||||
{ title: '分析頁', icon: mdiChartBoxOutline, path: '/analysis' },
|
||||
{
|
||||
title: '設定',
|
||||
icon: 'mdi-cog',
|
||||
icon: mdiCog,
|
||||
path: '/settings',
|
||||
navigable: false,
|
||||
subItems: [
|
||||
{ title: '角色管理', icon: 'mdi-account-group', path: '/role-management' },
|
||||
{ title: '選單管理', icon: 'mdi-menu', path: '/menu-management' },
|
||||
{ title: '部門管理', icon: 'mdi-domain', path: '/dept-management' },
|
||||
{ title: '角色管理', icon: mdiAccountGroup, path: '/role-management' },
|
||||
{ title: '選單管理', icon: mdiMenu, path: '/menu-management' },
|
||||
{ title: '部門管理', icon: mdiDomain, path: '/dept-management' },
|
||||
],
|
||||
},
|
||||
..._fixedMenuItems,
|
||||
@@ -305,10 +306,10 @@ function handleSearchSelect (item) {
|
||||
|
||||
// 訊息中心的示意資料,僅用於展示列表,不進行 API 呼叫
|
||||
const messageItems = [
|
||||
{ id: 1, title: '系統維護提醒', meta: '今天 09:00 · 資訊中心', icon: 'mdi-bell-outline' },
|
||||
{ id: 2, title: '教務處公告', meta: '昨天 16:20 · 教務處', icon: 'mdi-school-outline' },
|
||||
{ id: 3, title: '宿舍申請結果', meta: '昨天 13:05 · 學務處', icon: 'mdi-home-city-outline' },
|
||||
{ id: 4, title: '課表異動通知', meta: '2 天前 · 教務處', icon: 'mdi-calendar-outline' },
|
||||
{ id: 1, title: '系統維護提醒', meta: '今天 09:00 · 資訊中心', icon: mdiBellOutline },
|
||||
{ id: 2, title: '教務處公告', meta: '昨天 16:20 · 教務處', icon: mdiSchoolOutline },
|
||||
{ id: 3, title: '宿舍申請結果', meta: '昨天 13:05 · 學務處', icon: mdiHomeCityOutline },
|
||||
{ id: 4, title: '課表異動通知', meta: '2 天前 · 教務處', icon: mdiCalendarOutline },
|
||||
]
|
||||
|
||||
// v-data-iterator 會包裝 items,這裡取回原始資料物件
|
||||
@@ -436,7 +437,7 @@ const isCurrentFavorite = computed(() => favoritesStore.isFavorite(route.path))
|
||||
const isFavoriteActionDisabled = computed(() => !currentFavoriteInfo.value?.path || route.path === '/')
|
||||
|
||||
const favoriteActionLabel = computed(() => (isCurrentFavorite.value ? '移除常用' : '加入常用'))
|
||||
const favoriteActionIcon = computed(() => (isCurrentFavorite.value ? 'mdi-close-circle' : 'mdi-plus-circle'))
|
||||
const favoriteActionIcon = computed(() => (isCurrentFavorite.value ? mdiCloseCircle : mdiPlusCircle))
|
||||
|
||||
function toggleFavoriteItem (item) {
|
||||
if (!item?.path || item.path === '/') return
|
||||
@@ -474,7 +475,7 @@ function updateBreadcrumbs () {
|
||||
favoriteItems: mergedFavoriteItems.value,
|
||||
fallbackTitle,
|
||||
homeLabel: '首頁',
|
||||
homeIcon: 'mdi-home',
|
||||
homeIcon: mdiHome,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
元件 (Component)
|
||||
↓ 呼叫
|
||||
Store (Pinia) ← 管理狀態、快取
|
||||
↓ 呼叫
|
||||
API Service ← 封裝業務邏輯
|
||||
↓ 呼叫
|
||||
HTTP Client ← Axios 實例、攔截器
|
||||
|
||||
## 目前的資料流(以登入為例)
|
||||
|
||||
1. `views/Login.vue`(Playground 頁面)只負責表單/驗證碼/導頁等 UI 行為
|
||||
2. `stores/auth.ts` 統一負責登入狀態(`user`/`token`/`loading`/`error`)
|
||||
3. `services/modules/user.ts` 封裝 `login/getProfile/...` 端點
|
||||
4. `services/client.ts` 建立 `axios` instance
|
||||
5. `services/interceptors.ts` 統一注入 token 與處理 HTTP 錯誤
|
||||
|
||||
## Menu API 與資料結構
|
||||
|
||||
選單系統採用 API 驅動設計:
|
||||
|
||||
### API 端點
|
||||
|
||||
- `GET /service/api/menu`:取得完整選單樹
|
||||
- `GET /service/api/menu/favorite`:取得使用者收藏選單
|
||||
|
||||
### 資料結構
|
||||
|
||||
```ts
|
||||
interface MenuNode {
|
||||
mdl_id: string // 模組 ID
|
||||
mdl_name: string // 模組名稱
|
||||
unt_id?: string // 單位 ID
|
||||
unt_name?: string // 單位名稱
|
||||
fnc_id?: string // 功能 ID
|
||||
fnc_name?: string // 功能名稱
|
||||
children?: MenuNode[]
|
||||
}
|
||||
```
|
||||
|
||||
### 階層關係
|
||||
|
||||
- **第一層**:模組(mdl)
|
||||
- **第二層**:單位(unt)
|
||||
- **第三層**:功能(fnc),作為葉節點使用 `fnc_id` 作為路由路徑
|
||||
|
||||
### Store 持久化
|
||||
|
||||
`stores/menu.ts` 提供:
|
||||
- 自動 localStorage 持久化選單與收藏
|
||||
- 初始化時自動還原資料
|
||||
- 登出時清除快取
|
||||
|
||||
## API 前綴:`/api`
|
||||
|
||||
目前 Playground 已將 `api` 資料夾更名為 `services`,避免與 API 前綴 `/api` 衝突。
|
||||
|
||||
在開發模式下:
|
||||
|
||||
- 前端呼叫一律使用 `/service/api/*`
|
||||
- Vite dev server 透過 proxy 將 `/service/*` 轉送到後端(目前指向 `http://192.168.89.54:9002`)
|
||||
|
||||
## HTTP Client 設定
|
||||
|
||||
- `baseURL`:優先使用 `VITE_API_BASE_URL`,否則預設 `/service/api`(搭配 Vite proxy)
|
||||
- `Content-Type`:預設 `application/json`
|
||||
|
||||
## Token Service(單一來源)
|
||||
|
||||
為避免「Pinia token」與「localStorage token」不同步的問題,這裡採用單一來源:
|
||||
|
||||
- `services/token.ts` 使用 `ref` 保存 token,並同步 localStorage
|
||||
- Store 與 Interceptor 都只透過 `tokenService` 讀寫 token
|
||||
- 401 時清除 token,可立即同步到 UI
|
||||
|
||||
## Token 注入策略(Interceptor)
|
||||
|
||||
Interceptor 會從 `tokenService` 讀取 `token` 並注入 `Authorization: Bearer <token>`。
|
||||
|
||||
這樣做的原因是避免循環依賴:
|
||||
|
||||
`store(auth) -> services(userApi) -> httpClient -> interceptors -> store(auth)`
|
||||
|
||||
Store 仍然是「唯一負責更新 token 的地方」,Interceptor 只負責「讀取 token 並附加到 request」。
|
||||
|
||||
## 錯誤正規化(normalizeError)
|
||||
|
||||
為了讓 UI 不需要理解 AxiosError,這裡將錯誤統一成 `ApiRequestError`:
|
||||
|
||||
- `services/error.ts` 提供 `normalizeError()` 與 `ApiRequestError`
|
||||
- Interceptor 在 response error 時呼叫 `normalizeError()`
|
||||
- Store 只需要處理 `error.message / error.code / error.status`
|
||||
|
||||
最低限度的映射規則:
|
||||
|
||||
- 有 `response.data.message` 優先使用
|
||||
- 其次使用 `AxiosError.message`
|
||||
- 都沒有則顯示 `請求失敗`
|
||||
|
||||
## 請求取消(AbortController)
|
||||
|
||||
取消策略採「同類型請求互斥」,目前示範在 `login`:
|
||||
|
||||
- Store 建立 `AbortController`,每次登入前先取消前一次
|
||||
- Service 只接收 `signal`,不管理 controller 狀態
|
||||
- `normalizeError` 會將取消行為轉為 `CanceledRequestError`
|
||||
- UI 不顯示取消造成的錯誤訊息
|
||||
|
||||
| DECISION | WHY | WHY NOT |
|
||||
|---|---|---|
|
||||
| Store 呼叫 API,不是元件直接呼叫 | 狀態集中管理、可快取、易測試 | 元件直接呼叫會導致狀態散落 |
|
||||
| API 模組化(userApi、orderApi)| 關注點分離、好維護 | 全塞一個檔案會變超大 |
|
||||
| Interceptor 獨立檔案| 單一職責、好測試 | 寫在 client.ts 會雜亂 |
|
||||
| 泛型 ApiResponse<T>| 型別安全、IDE 提示 | 用 any 會失去 TS 優勢 |
|
||||
| API 前綴使用 `/api` | 使用習慣一致、搭配 Vite proxy 容易理解 | 若前端資料夾也叫 `api` 容易造成路徑衝突,因此此專案改名為 `services` |
|
||||
@@ -1,3 +1,4 @@
|
||||
import { mdiHome } from '@mdi/js'
|
||||
import type { LayoutMenuItem } from './menu'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
@@ -64,7 +65,7 @@ function toBreadcrumbItems (trail: LayoutMenuItem[],
|
||||
export const useBreadcrumbStore = defineStore('breadcrumbs', () => {
|
||||
const items = ref<BreadcrumbItem[]>([])
|
||||
const homeLabel = ref('首頁')
|
||||
const homeIcon = ref('mdi-home')
|
||||
const homeIcon = ref(mdiHome)
|
||||
|
||||
const setBreadcrumbs = (payload: BreadcrumbPayload) => {
|
||||
if (!payload?.path) return
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { mdiAccountSchool, mdiBookOpenPageVariant, mdiChartPie, mdiCheckDecagram, mdiCloudDownload } from '@mdi/js'
|
||||
import { ref } from 'vue'
|
||||
import SKAnalysis from '@/components/SKAnalysis.vue'
|
||||
|
||||
@@ -21,7 +22,7 @@ const stats = ref([
|
||||
{
|
||||
title: '學生總數',
|
||||
value: '2,580',
|
||||
icon: 'mdi-account-school',
|
||||
icon: mdiAccountSchool,
|
||||
color: 'primary',
|
||||
label: '總學籍人數',
|
||||
total: '120,000',
|
||||
@@ -29,7 +30,7 @@ const stats = ref([
|
||||
{
|
||||
title: '平台訪問',
|
||||
value: '20,000',
|
||||
icon: 'mdi-chart-pie',
|
||||
icon: mdiChartPie,
|
||||
color: 'error',
|
||||
label: '今日訪問量',
|
||||
total: '500,000',
|
||||
@@ -37,7 +38,7 @@ const stats = ref([
|
||||
{
|
||||
title: '教材下載',
|
||||
value: '8,000',
|
||||
icon: 'mdi-cloud-download',
|
||||
icon: mdiCloudDownload,
|
||||
color: 'warning',
|
||||
label: '本月下載次數',
|
||||
total: '120,000',
|
||||
@@ -45,7 +46,7 @@ const stats = ref([
|
||||
{
|
||||
title: '圖書館借閱',
|
||||
value: '5,000',
|
||||
icon: 'mdi-book-open-page-variant',
|
||||
icon: mdiBookOpenPageVariant,
|
||||
color: 'success',
|
||||
label: '總借閱量',
|
||||
total: '50,000',
|
||||
@@ -76,6 +77,6 @@ const pie2Data = ref({
|
||||
value: 92,
|
||||
label: '全校平均',
|
||||
color: 'teal-lighten-1',
|
||||
icon: 'mdi-check-decagram',
|
||||
icon: mdiCheckDecagram,
|
||||
})
|
||||
</script>
|
||||
|
||||
+13
-12
@@ -10,6 +10,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { mdiAccountGroup, mdiBookOpenVariant, mdiCalendarCheck, mdiChartBar, mdiCog, mdiHammerWrench, mdiHome, mdiLayers, mdiLock, mdiMonitorShimmer, mdiSchool, mdiViewDashboard } from '@mdi/js'
|
||||
import { ref } from 'vue'
|
||||
import SKDashboard from '@/components/SKDashboard.vue'
|
||||
|
||||
@@ -21,7 +22,7 @@ const greetingTitle = ref('早安,王校長,開始您一天的工作吧!')
|
||||
const applications = ref([
|
||||
{
|
||||
name: '校務行政系統',
|
||||
icon: 'mdi-school',
|
||||
icon: mdiSchool,
|
||||
desc: '全校教職員工生學籍資料、人事資料、財產管理等核心系統入口。',
|
||||
group: '行政組',
|
||||
date: '2025-01-05',
|
||||
@@ -29,7 +30,7 @@ const applications = ref([
|
||||
},
|
||||
{
|
||||
name: '數位學習平台',
|
||||
icon: 'mdi-monitor-shimmer',
|
||||
icon: mdiMonitorShimmer,
|
||||
desc: '提供線上課程、作業繳交、測驗評量與師生互動討論功能。',
|
||||
group: '教學組',
|
||||
date: '2025-01-02',
|
||||
@@ -37,7 +38,7 @@ const applications = ref([
|
||||
},
|
||||
{
|
||||
name: '圖書館系統',
|
||||
icon: 'mdi-book-open-variant',
|
||||
icon: mdiBookOpenVariant,
|
||||
desc: '館藏查詢、圖書借閱、還書預約與電子書資源整合平台。',
|
||||
group: '圖書館',
|
||||
date: '2024-12-28',
|
||||
@@ -45,7 +46,7 @@ const applications = ref([
|
||||
},
|
||||
{
|
||||
name: '學生請假系統',
|
||||
icon: 'mdi-calendar-check',
|
||||
icon: mdiCalendarCheck,
|
||||
desc: '學生線上請假申請、導師審核、生輔組備查流程電子化。',
|
||||
group: '學務處',
|
||||
date: '2024-12-25',
|
||||
@@ -53,7 +54,7 @@ const applications = ref([
|
||||
},
|
||||
{
|
||||
name: '報修系統',
|
||||
icon: 'mdi-hammer-wrench',
|
||||
icon: mdiHammerWrench,
|
||||
desc: '校園設施設備故障通報、維修進度查詢與滿意度調查。',
|
||||
group: '總務處',
|
||||
date: '2024-12-20',
|
||||
@@ -61,7 +62,7 @@ const applications = ref([
|
||||
},
|
||||
{
|
||||
name: '會議室預約',
|
||||
icon: 'mdi-account-group',
|
||||
icon: mdiAccountGroup,
|
||||
desc: '校內各大型會議室、視聽教室場地查詢與線上預約登記。',
|
||||
group: '總務處',
|
||||
date: '2024-12-15',
|
||||
@@ -101,12 +102,12 @@ const announcements = ref([
|
||||
])
|
||||
|
||||
const quickNavs = ref([
|
||||
{ title: '首頁', icon: 'mdi-home', color: 'primary' },
|
||||
{ title: '控制台', icon: 'mdi-view-dashboard', color: 'error' },
|
||||
{ title: '組件', icon: 'mdi-layers', color: 'warning' },
|
||||
{ title: '系統管理', icon: 'mdi-cog', color: 'success' },
|
||||
{ title: '權限', icon: 'mdi-lock', color: 'purple' },
|
||||
{ title: '圖表', icon: 'mdi-chart-bar', color: 'info' },
|
||||
{ title: '首頁', icon: mdiHome, color: 'primary' },
|
||||
{ title: '控制台', icon: mdiViewDashboard, color: 'error' },
|
||||
{ title: '組件', icon: mdiLayers, color: 'warning' },
|
||||
{ title: '系統管理', icon: mdiCog, color: 'success' },
|
||||
{ title: '權限', icon: mdiLock, color: 'purple' },
|
||||
{ title: '圖表', icon: mdiChartBar, color: 'info' },
|
||||
])
|
||||
|
||||
const todos = ref([
|
||||
|
||||
+3
-2
@@ -48,11 +48,11 @@ v-if="resolveNewsItem(wrapped).isNew" class="ml-2" color="primary" size="x-small
|
||||
</div>
|
||||
<div class="d-flex ga-4 mt-3 text-caption text-medium-emphasis">
|
||||
<div class="d-flex align-center ga-1">
|
||||
<v-icon size="14">mdi-folder-outline</v-icon>
|
||||
<v-icon size="14" :icon="mdiFolderOutline" />
|
||||
<span>{{ resolveNewsItem(wrapped).dept }}</span>
|
||||
</div>
|
||||
<div class="d-flex align-center ga-1">
|
||||
<v-icon size="14">mdi-eye-outline</v-icon>
|
||||
<v-icon size="14" :icon="mdiEyeOutline" />
|
||||
<span>{{ resolveNewsItem(wrapped).views }} 次瀏覽</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -119,6 +119,7 @@ class="d-flex flex-column align-center ga-2 text-center py-4 px-2 quick-item" va
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { mdiEyeOutline, mdiFolderOutline } from '@mdi/js'
|
||||
import { ref } from 'vue'
|
||||
import { useMessageStore } from '@/stores/messages'
|
||||
import { useSnackbarStore } from '@/stores/snackbar'
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<v-col cols="12" md="8" sm="10">
|
||||
<v-card border class="pa-6" variant="flat">
|
||||
<v-card-title class="d-flex align-center ga-3">
|
||||
<v-icon :color="color" size="36">{{ icon }}</v-icon>
|
||||
<v-icon :color="color" size="36" :icon="icon" />
|
||||
<div class="text-h5">{{ title }}</div>
|
||||
<div class="text-caption text-medium-emphasis">{{ codeLabel }}</div>
|
||||
<v-spacer />
|
||||
@@ -35,6 +35,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { mdiAlertCircleOutline } from '@mdi/js'
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
@@ -51,7 +52,7 @@ type Props = {
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
description: undefined,
|
||||
icon: 'mdi-alert-circle-outline',
|
||||
icon: mdiAlertCircleOutline,
|
||||
color: 'warning',
|
||||
showHome: true,
|
||||
showLogin: true,
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
:code="403"
|
||||
color="warning"
|
||||
description="你沒有權限存取此頁面或操作。"
|
||||
icon="mdi-shield-lock-outline"
|
||||
:icon="mdiShieldLockOutline"
|
||||
title="沒有權限"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { mdiShieldLockOutline } from '@mdi/js'
|
||||
import ErrorShell from './ErrorShell.vue'
|
||||
</script>
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
code="MAINTENANCE"
|
||||
color="info"
|
||||
description="目前系統正在維護,請稍後再試。"
|
||||
icon="mdi-tools"
|
||||
:icon="mdiTools"
|
||||
:show-login="false"
|
||||
title="系統維護中"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { mdiTools } from '@mdi/js'
|
||||
import ErrorShell from './ErrorShell.vue'
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
code="NETWORK"
|
||||
color="warning"
|
||||
description="無法連線到伺服器,請檢查網路或稍後再試。"
|
||||
icon="mdi-wifi-off"
|
||||
:icon="mdiWifiOff"
|
||||
title="網路連線異常"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { mdiWifiOff } from '@mdi/js'
|
||||
import ErrorShell from './ErrorShell.vue'
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
:code="404"
|
||||
color="info"
|
||||
description="你輸入的網址不存在,或頁面已被移除。"
|
||||
icon="mdi-map-marker-question-outline"
|
||||
:icon="mdiMapMarkerQuestionOutline"
|
||||
:show-login="false"
|
||||
title="找不到頁面"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { mdiMapMarkerQuestionOutline } from '@mdi/js'
|
||||
import ErrorShell from './ErrorShell.vue'
|
||||
</script>
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
:code="500"
|
||||
color="error"
|
||||
description="伺服器發生非預期錯誤,請稍後再試。"
|
||||
icon="mdi-server"
|
||||
:icon="mdiServer"
|
||||
title="系統發生錯誤"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { mdiServer } from '@mdi/js'
|
||||
import ErrorShell from './ErrorShell.vue'
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
:code="503"
|
||||
color="warning"
|
||||
description="服務目前無法使用,請稍後再試。"
|
||||
icon="mdi-server-off"
|
||||
:icon="mdiServerOff"
|
||||
title="服務暫時無法使用"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { mdiServerOff } from '@mdi/js'
|
||||
import ErrorShell from './ErrorShell.vue'
|
||||
</script>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</v-chip>
|
||||
<v-chip color="info" variant="tonal">筆數 {{ filteredStudents.length }}</v-chip>
|
||||
<v-spacer />
|
||||
<v-btn flat prepend-icon="mdi-magnify" @click="isSearchVisible = !isSearchVisible">條件搜尋</v-btn>
|
||||
<v-btn flat :prepend-icon="mdiMagnify" @click="isSearchVisible = !isSearchVisible">條件搜尋</v-btn>
|
||||
<v-switch v-model="isBulkEditEnabled" color="primary" hide-details label="啟用編輯" />
|
||||
</v-card-title>
|
||||
|
||||
@@ -40,16 +40,16 @@ v-model="search.department" :class="{ 'select-hide-arrow': !isBulkEditEnabled }"
|
||||
|
||||
<!-- 操作 -->
|
||||
<v-row v-if="isBulkEditEnabled" align="center" class="mb-2 ga-1" dense>
|
||||
<v-btn :disabled="!hasSelectedRows" prepend-icon="mdi-delete" variant="outlined" @click="deleteSelectedRows">
|
||||
<v-btn :disabled="!hasSelectedRows" :prepend-icon="mdiDelete" variant="outlined" @click="deleteSelectedRows">
|
||||
批次刪除
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary" :disabled="!hasAnyChange" prepend-icon="mdi-content-save" variant="outlined"
|
||||
color="primary" :disabled="!hasAnyChange" :prepend-icon="mdiContentSave" variant="outlined"
|
||||
@click="saveAllRows">
|
||||
儲存變更
|
||||
</v-btn>
|
||||
<v-btn :disabled="!hasAnyChange" prepend-icon="mdi-restore" variant="text" @click="resetAllRows">
|
||||
<v-btn :disabled="!hasAnyChange" :prepend-icon="mdiRestore" variant="text" @click="resetAllRows">
|
||||
取消變更
|
||||
</v-btn>
|
||||
</v-row>
|
||||
@@ -142,6 +142,7 @@ color="error" :disabled="!isBulkEditEnabled" size="small" variant="text"
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { mdiContentSave, mdiDelete, mdiMagnify, mdiRestore } from '@mdi/js'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { type StudentRecord, useStudentStore } from '@/stores/students'
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ v-model="search.grade" density="compact" hide-details item-title="title" item-va
|
||||
<v-select v-model="search.status" density="compact" hide-details :items="statuses" variant="outlined" />
|
||||
</v-col>
|
||||
<v-col class="d-flex justify-end align-end flex-grow-1 ga-2" cols="12" md="auto">
|
||||
<v-btn prepend-icon="mdi-broom" variant="text" @click="resetSearch">清除</v-btn>
|
||||
<v-btn color="primary" disabled prepend-icon="mdi-magnify" variant="tonal">查詢</v-btn>
|
||||
<v-btn :prepend-icon="mdiBroom" variant="text" @click="resetSearch">清除</v-btn>
|
||||
<v-btn color="primary" disabled :prepend-icon="mdiMagnify" variant="tonal">查詢</v-btn>
|
||||
</v-col>
|
||||
</template>
|
||||
<template #table>
|
||||
@@ -47,14 +47,14 @@ class="student-table" density="compact" fixed-header :headers="tableHeaders" hei
|
||||
</template>
|
||||
<template #[`item.actions`]="{ item }">
|
||||
<div class="d-flex ga-1">
|
||||
<v-btn color="info" prepend-icon="mdi-eye" size="small" variant="text" @click="openViewDialog(item)">
|
||||
<v-btn color="info" :prepend-icon="mdiEye" size="small" variant="text" @click="openViewDialog(item)">
|
||||
檢視
|
||||
</v-btn>
|
||||
<v-btn color="primary" prepend-icon="mdi-pencil" size="small" variant="text" @click="openEditDialog(item)">
|
||||
<v-btn color="primary" :prepend-icon="mdiPencil" size="small" variant="text" @click="openEditDialog(item)">
|
||||
修改
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error" prepend-icon="mdi-delete" size="small" variant="text"
|
||||
color="error" :prepend-icon="mdiDelete" size="small" variant="text"
|
||||
@click="requestDeleteConfirmation(item)">
|
||||
刪除
|
||||
</v-btn>
|
||||
@@ -79,11 +79,17 @@ class="dialog-overlay" :close-on-content-click="false" :model-value="dialogVisib
|
||||
v-if="!isMobile || activeMobilePanel === 'detail'" class="detail-panel-wrapper"
|
||||
:class="{ 'is-active': !!selectedSemesterId, 'is-mobile': isMobile }">
|
||||
<master-detail-semester-panel
|
||||
:detail-form="detailForm" :is-detail-editing="isDetailEditing"
|
||||
:is-mobile="isMobile" :is-view-mode="isViewMode" :selected-semester="selectedSemester"
|
||||
@add-course="addCourseToDetail" @cancel-edit="cancelDetailEdit" @close="closeDetailPanel"
|
||||
@delete="handleDeleteSemester" @remove-course="removeCourseFromDetail" @save-edit="saveDetailEdit"
|
||||
@start-edit="startDetailEdit" />
|
||||
v-model:detail-form="detailForm"
|
||||
:is-detail-editing="isDetailEditing"
|
||||
:is-mobile="isMobile"
|
||||
:is-view-mode="isViewMode"
|
||||
:selected-semester="selectedSemester"
|
||||
@cancel-edit="cancelDetailEdit"
|
||||
@close="closeDetailPanel"
|
||||
@delete="handleDeleteSemester"
|
||||
@save-edit="saveDetailEdit"
|
||||
@start-edit="startDetailEdit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 主檔區塊 (Master Card):學生基本資料與學期列表 -->
|
||||
@@ -257,6 +263,7 @@ v-model="confirmNavigateVisible" confirm-text="確定切換" max-width="480"
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { mdiBroom, mdiDelete, mdiEye, mdiMagnify, mdiPencil } from '@mdi/js'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ v-model="search.grade" density="compact" hide-details item-title="title" item-va
|
||||
<v-select v-model="search.status" density="compact" hide-details :items="statuses" variant="outlined" />
|
||||
</v-col>
|
||||
<v-col class="d-flex justify-end align-end flex-grow-1 ga-2" cols="12" md="auto">
|
||||
<v-btn prepend-icon="mdi-broom" variant="text" @click="resetSearch">清除</v-btn>
|
||||
<v-btn color="primary" disabled prepend-icon="mdi-magnify" variant="tonal">查詢</v-btn>
|
||||
<v-btn :prepend-icon="mdiBroom" variant="text" @click="resetSearch">清除</v-btn>
|
||||
<v-btn color="primary" disabled :prepend-icon="mdiMagnify" variant="tonal">查詢</v-btn>
|
||||
</v-col>
|
||||
</template>
|
||||
<template #table>
|
||||
@@ -47,14 +47,14 @@ class="student-table" density="compact" fixed-header :headers="tableHeaders" hei
|
||||
</template>
|
||||
<template #[`item.actions`]="{ item }">
|
||||
<div class="d-flex ga-2">
|
||||
<v-btn color="info" prepend-icon="mdi-eye" size="small" variant="text" @click="openViewDialog(item)">
|
||||
<v-btn color="info" :prepend-icon="mdiEye" size="small" variant="text" @click="openViewDialog(item)">
|
||||
檢視
|
||||
</v-btn>
|
||||
<v-btn color="primary" prepend-icon="mdi-pencil" size="small" variant="text" @click="openEditDialog(item)">
|
||||
<v-btn color="primary" :prepend-icon="mdiPencil" size="small" variant="text" @click="openEditDialog(item)">
|
||||
修改
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error" prepend-icon="mdi-delete" size="small" variant="text"
|
||||
color="error" :prepend-icon="mdiDelete" size="small" variant="text"
|
||||
@click="requestDeleteConfirmation(item)">
|
||||
刪除
|
||||
</v-btn>
|
||||
@@ -255,7 +255,7 @@ v-model="confirmDeleteCourseVisible" confirm-color="error"
|
||||
<v-dialog v-model="addCourseDialogVisible" max-width="420" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon color="primary" start>mdi-book-plus</v-icon>
|
||||
<v-icon color="primary" start :icon="mdiBookPlus" />
|
||||
加入課程
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
@@ -290,6 +290,7 @@ v-model.number="addCourseForm.score" density="comfortable" hide-spin-buttons lab
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { mdiBookPlus, mdiBroom, mdiDelete, mdiEye, mdiMagnify, mdiPencil } from '@mdi/js'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ v-model="search.grade" density="compact" hide-details item-title="title" item-va
|
||||
<v-select v-model="search.status" density="compact" hide-details :items="statuses" variant="outlined" />
|
||||
</v-col>
|
||||
<v-col class="d-flex justify-end align-end flex-grow-1 ga-2" cols="12" md="auto">
|
||||
<v-btn prepend-icon="mdi-broom" variant="text" @click="resetSearch">清除</v-btn>
|
||||
<v-btn color="primary" disabled prepend-icon="mdi-magnify" variant="tonal">查詢</v-btn>
|
||||
<v-btn :prepend-icon="mdiBroom" variant="text" @click="resetSearch">清除</v-btn>
|
||||
<v-btn color="primary" disabled :prepend-icon="mdiMagnify" variant="tonal">查詢</v-btn>
|
||||
</v-col>
|
||||
</template>
|
||||
<template #table>
|
||||
@@ -47,14 +47,14 @@ class="student-table" density="compact" fixed-header :headers="tableHeaders" hei
|
||||
</template>
|
||||
<template #[`item.actions`]="{ item }">
|
||||
<div class="d-flex ga-2">
|
||||
<v-btn color="info" prepend-icon="mdi-eye" size="small" variant="text" @click="openViewDialog(item)">
|
||||
<v-btn color="info" :prepend-icon="mdiEye" size="small" variant="text" @click="openViewDialog(item)">
|
||||
檢視
|
||||
</v-btn>
|
||||
<v-btn color="primary" prepend-icon="mdi-pencil" size="small" variant="text" @click="openEditDialog(item)">
|
||||
<v-btn color="primary" :prepend-icon="mdiPencil" size="small" variant="text" @click="openEditDialog(item)">
|
||||
修改
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error" prepend-icon="mdi-delete" size="small" variant="text"
|
||||
color="error" :prepend-icon="mdiDelete" size="small" variant="text"
|
||||
@click="requestDeleteConfirmation(item)">
|
||||
刪除
|
||||
</v-btn>
|
||||
@@ -253,7 +253,7 @@ v-model="confirmNavigateVisible" confirm-text="確定切換" max-width="480"
|
||||
<v-dialog v-model="addCourseDialogVisible" max-width="480" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon color="primary" start>mdi-school</v-icon>
|
||||
<v-icon color="primary" start :icon="mdiSchool" />
|
||||
新增成績
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
@@ -280,6 +280,7 @@ v-model.number="addCourseForm.score" density="comfortable" label="分數" type="
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { mdiBroom, mdiDelete, mdiEye, mdiMagnify, mdiPencil, mdiSchool } from '@mdi/js'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ v-model="search.grade" density="compact" hide-details item-title="title" item-va
|
||||
<v-select v-model="search.status" density="compact" hide-details :items="statuses" variant="outlined" />
|
||||
</v-col>
|
||||
<v-col class="d-flex justify-end align-end flex-grow-1 ga-2" cols="12" md="auto">
|
||||
<v-btn prepend-icon="mdi-broom" variant="text" @click="resetSearch">清除</v-btn>
|
||||
<v-btn color="primary" disabled prepend-icon="mdi-magnify" variant="tonal">查詢</v-btn>
|
||||
<v-btn :prepend-icon="mdiBroom" variant="text" @click="resetSearch">清除</v-btn>
|
||||
<v-btn color="primary" disabled :prepend-icon="mdiMagnify" variant="tonal">查詢</v-btn>
|
||||
</v-col>
|
||||
</template>
|
||||
<template #table>
|
||||
@@ -47,14 +47,14 @@ class="student-table" density="compact" fixed-header :headers="tableHeaders"
|
||||
</template>
|
||||
<template #[`item.actions`]="{ item }">
|
||||
<div class="d-flex ga-2">
|
||||
<v-btn color="info" prepend-icon="mdi-eye" size="small" variant="text" @click="openViewDialog(item)">
|
||||
<v-btn color="info" :prepend-icon="mdiEye" size="small" variant="text" @click="openViewDialog(item)">
|
||||
檢視
|
||||
</v-btn>
|
||||
<v-btn color="primary" prepend-icon="mdi-pencil" size="small" variant="text" @click="openEditDialog(item)">
|
||||
<v-btn color="primary" :prepend-icon="mdiPencil" size="small" variant="text" @click="openEditDialog(item)">
|
||||
修改
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error" prepend-icon="mdi-delete" size="small" variant="text"
|
||||
color="error" :prepend-icon="mdiDelete" size="small" variant="text"
|
||||
@click="requestDeleteConfirmation(item)">
|
||||
刪除
|
||||
</v-btn>
|
||||
@@ -238,6 +238,7 @@ v-model="confirmNavigateVisible" confirm-text="確定切換" max-width="480"
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { mdiBroom, mdiDelete, mdiEye, mdiMagnify, mdiPencil } from '@mdi/js'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import CommonConfirmDialog from '@/components/maintenance/CommonConfirmDialog.vue'
|
||||
|
||||
Reference in New Issue
Block a user