feat: add SingleRecordMnt component for student record maintenance with search, add, edit, view, and delete functionalities

This commit is contained in:
skytek_xinliang
2026-03-26 11:24:37 +08:00
parent 507afcc99c
commit 069141794e
116 changed files with 15247 additions and 107 deletions
+245
View File
@@ -0,0 +1,245 @@
<template>
<v-container class="pa-0" fluid>
<div class="d-flex flex-column ga-5 py-4 pr-2 pl-0">
<v-sheet class="d-flex flex-column flex-sm-row align-start align-sm-center ga-4 pa-5 elevation-1" color="surface">
<v-avatar color="primary" size="52" variant="tonal">
<span class="text-h5">👋</span>
</v-avatar>
<div>
<div class="text-h5 font-weight-bold">歡迎使用校務資訊系統</div>
<div class="text-body-2 text-medium-emphasis mt-1">
使用頂部搜尋框快速找到功能或從左側選單瀏覽所有系統模組
</div>
</div>
</v-sheet>
<section class="d-flex flex-column">
<div class="d-flex align-center ga-2 text-h6 font-weight-bold">📰 最新消息</div>
<!--
使用 v-data-iterator 讓消息列表具備一致的資料迭代結構
也方便未來需要加上排序 / 分頁時直接擴充
-->
<v-data-iterator class="mt-2" item-key="id" :items="newsItems" :items-per-page="-1">
<!--
v-data-iterator default slot 會提供包裝後的 items
這裡透過 resolveNewsItem 抽出原始資料再沿用原本的卡片排版
-->
<template #default="{ items }">
<v-row dense>
<v-col v-for="wrapped in items" :key="resolveNewsItem(wrapped).id" cols="12">
<v-card
class="news-item d-flex flex-column flex-sm-row ga-4 pa-4 bg-surface" variant="outlined"
@click="handleNews(resolveNewsItem(wrapped))">
<v-sheet class="news-badge">
<div class="news-badge-date">{{ resolveNewsItem(wrapped).date }}</div>
<div class="news-badge-month">{{ resolveNewsItem(wrapped).month }}</div>
</v-sheet>
<div class="flex-grow-1">
<div class="d-flex flex-wrap align-center font-weight-bold">
{{ resolveNewsItem(wrapped).title }}
<v-chip
v-if="resolveNewsItem(wrapped).isNew" class="ml-2" color="primary" size="x-small"
variant="flat">
NEW
</v-chip>
</div>
<div class="text-body-2 text-medium-emphasis mt-2">
{{ resolveNewsItem(wrapped).desc }}
</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>
<span>{{ resolveNewsItem(wrapped).dept }}</span>
</div>
<div class="d-flex align-center ga-1">
<v-icon size="14">mdi-eye-outline</v-icon>
<span>{{ resolveNewsItem(wrapped).views }} 次瀏覽</span>
</div>
</div>
</div>
</v-card>
</v-col>
</v-row>
</template>
</v-data-iterator>
</section>
<v-card
class="d-flex align-center justify-space-between ga-3 px-5 py-4" color="secondary" rounded="xl"
variant="tonal" @click="handleMessageCenter">
<div class="d-flex align-center ga-4">
<v-avatar color="secondary" size="44" variant="flat">
<span class="text-h6"></span>
</v-avatar>
<div>
<div class="text-subtitle-1 font-weight-bold">訊息中心</div>
<div class="text-body-2 font-weight-bold text-on-secondary">12 筆未讀</div>
</div>
</div>
<div class="text-body-2 font-weight-medium">查看全部 </div>
</v-card>
<section class="d-flex flex-column pb-4">
<div class="d-flex align-center ga-2 text-h6 font-weight-bold">🚀 快速存取</div>
<v-row class="mt-2" dense>
<v-col v-for="item in quickItems" :key="item.title" cols="6" md="2" sm="4">
<v-card
class="d-flex flex-column align-center ga-2 text-center py-4 px-2 quick-item" variant="outlined"
@click="handleQuick(item)">
<div class="text-h5">{{ item.icon }}</div>
<div class="text-body-2 font-weight-medium">{{ item.title }}</div>
</v-card>
</v-col>
</v-row>
</section>
</div>
<!--
點擊消息後顯示的對話框
僅負責呈現內容不做任何延伸操作確保互動行為單純可預期
-->
<v-dialog v-model="isNewsDialogOpen" max-width="640">
<v-card v-if="selectedNews">
<v-card-title class="text-h6 font-weight-bold bg-primary-variant pa-4">
{{ selectedNews.title }}
</v-card-title>
<v-card-subtitle class="text-body-2 pt-4 text-medium-emphasis">
{{ selectedNews.month }} {{ selectedNews.date }} · {{ selectedNews.dept }} ·
{{ selectedNews.views }} 次瀏覽
</v-card-subtitle>
<v-card-text class="pt-4">
{{ selectedNews.desc }}
</v-card-text>
<v-card-actions class="justify-end">
<v-btn color="primary" variant="text" @click="isNewsDialogOpen = false">關閉</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useMessageStore } from '@/stores/messages'
import { useSnackbarStore } from '@/stores/snackbar'
const snackbar = useSnackbarStore()
const messageStore = useMessageStore()
const newsItems = [
{
id: 1,
date: '29',
month: '1月',
title: '113學年度第2學期加退選開始',
desc: '加退選時間為1月29日至2月9日止,請同學把握時間完成選課作業。',
dept: '教務處',
views: '1,234',
isNew: true,
},
{
id: 2,
date: '27',
month: '1月',
title: '場地借用系統維護通知',
desc: '系統將於本週六凌晨01:00-05:00進行維護,期間暫停服務。',
dept: '總務處',
views: '856',
isNew: false,
},
{
id: 3,
date: '25',
month: '1月',
title: '112學年度第1學期期末成績已開放查詢',
desc: '同學可至「查詢 > 教務資訊查詢 > 學期成績查詢」查看本學期成績。',
dept: '教務處',
views: '3,567',
isNew: false,
},
]
type NewsItem = (typeof newsItems)[number]
const quickItems = [
{ icon: '', title: '線上加選' },
{ icon: '', title: '線上退選' },
{ icon: '📊', title: '成績查詢' },
{ icon: '📅', title: '個人課表' },
{ icon: '📝', title: '網路請假' },
{ icon: '🏢', title: '場地借用' },
]
const selectedNews = ref<NewsItem | null>(null)
const isNewsDialogOpen = ref(false)
// v-data-iterator 會包裝 items,這個方法用來安全地取回原始資料結構。
function resolveNewsItem (wrapped: unknown): NewsItem {
if (wrapped && typeof wrapped === 'object' && 'raw' in wrapped) {
return (wrapped as { raw: NewsItem }).raw
}
return wrapped as NewsItem
}
function handleNews (item: NewsItem) {
selectedNews.value = item
isNewsDialogOpen.value = true
}
// 點擊首頁「訊息中心」卡片,開啟共用的訊息清單 dialog
function handleMessageCenter () {
messageStore.open()
}
function handleQuick (item: (typeof quickItems)[number]) {
snackbar.show({ message: `前往:${item.title}`, color: 'info' })
}
</script>
<style scoped>
.news-item {
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.news-item:hover {
transform: translateY(-2px);
box-shadow: 0 10px 24px rgba(var(--v-theme-on-surface), 0.12);
}
.news-badge {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgb(var(--v-theme-primary));
color: rgb(var(--v-theme-on-primary));
border-radius: 12px;
padding: 10px 6px;
min-height: 64px;
min-width: 64px;
}
.news-badge-date {
font-size: 20px;
font-weight: 700;
line-height: 1;
}
.news-badge-month {
font-size: 12px;
margin-top: 4px;
}
.quick-item {
display: flex;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.quick-item:hover {
transform: translateY(-2px);
box-shadow: 0 10px 24px rgba(var(--v-theme-on-surface), 0.12);
}
</style>