Files
gc-plan/week7/frontend/src/views/StudentListView.vue
2026-04-29 23:45:17 +08:00

209 lines
7.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 学生列表页Day 3-6CRUD + 分页 + 搜索 + Loading + 空状态 + 错误处理-->
<template>
<div>
<NavBar />
<div class="page">
<!-- 工具栏Day 3 搜索 + Day 4 分页 -->
<div class="toolbar">
<input v-model="keyword" placeholder="搜索姓名..." @keyup.enter="search" />
<button class="btn-outline" @click="search">搜索</button>
<button class="btn-outline" @click="refresh">刷新</button>
<button v-if="auth.isAdmin" class="btn-add" @click="openForm()">+ 新增</button>
</div>
<!-- 错误提示Day 6 -->
<div class="error-bar" v-if="error">{{ error }}<button @click="refresh">重试</button></div>
<!-- Loading 状态Day 6 -->
<div class="loading" v-if="loading">加载中...</div>
<!-- 空状态Day 6 -->
<div class="empty" v-else-if="!loading && students.length === 0">
暂无数据
</div>
<!-- 表格 -->
<div class="table-wrap" v-else>
<table>
<thead>
<tr><th>ID</th><th>姓名</th><th>年龄</th><th>邮箱</th><th>成绩</th><th v-if="auth.isAdmin">操作</th></tr>
</thead>
<tbody>
<tr v-for="s in students" :key="s.id">
<td>{{ s.id }}</td>
<td><strong>{{ s.name }}</strong></td>
<td>{{ s.age }}</td>
<td>{{ s.email }}</td>
<td><span class="badge" :class="scoreClass(s.score)">{{ s.score }}</span></td>
<td v-if="auth.isAdmin">
<button class="btn-edit" @click="openForm(s)">编辑</button>
<button class="btn-del" @click="handleDelete(s.id)">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页组件Day 4 -->
<Pagination :current="page" :totalPages="totalPages" @change="goPage" />
<!-- 统计卡片 -->
<div class="stats">
<div class="stat"><span class="v">{{ statsTotal }}</span><span class="l">总数</span></div>
<div class="stat"><span class="v">{{ statsExcellent }}</span><span class="l">优秀</span></div>
<div class="stat"><span class="v">{{ orm }}</span><span class="l">ORM</span></div>
</div>
<!-- 新增/编辑弹窗Day 3 + Day 5表单 + 提交 -->
<StudentForm
:visible="formVisible"
:student="editTarget"
:loading="formLoading"
@close="formVisible = false"
@submit="handleFormSubmit"
/>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useAuthStore } from '../stores/auth'
import { fetchStudents, fetchExcellent, createStudent, updateStudent, deleteStudent } from '../api/student'
import NavBar from '../components/NavBar.vue'
import Pagination from '../components/Pagination.vue'
import StudentForm from '../components/StudentForm.vue'
const auth = useAuthStore()
// 列表状态
const students = ref([])
const keyword = ref('')
const page = ref(0)
const size = ref(10)
const totalPages = ref(0)
const loading = ref(false)
const error = ref('')
const statsTotal = ref(0)
const statsExcellent = ref(0)
const orm = ref('--')
// 表单状态
const formVisible = ref(false)
const editTarget = ref(null)
const formLoading = ref(false)
function scoreClass(s) {
if (s >= 90) return 'badge-excellent'
if (s >= 80) return 'badge-good'
if (s >= 60) return 'badge-normal'
return 'badge-poor'
}
async function loadData() {
loading.value = true; error.value = ''
try {
const result = await fetchStudents(keyword.value || undefined, page.value, size.value)
students.value = result.data || []
statsTotal.value = result.extra?.total ?? students.value.length
if (result.extra?.pages != null) totalPages.value = result.extra.pages
else totalPages.value = Math.ceil((result.extra?.total ?? students.value.length) / size.value)
orm.value = result.extra?.orm ?? '--'
const exc = await fetchExcellent(85)
statsExcellent.value = exc.data ?? 0
} catch (e) {
error.value = e.message
students.value = []
} finally {
loading.value = false
}
}
function search() { page.value = 0; loadData() }
function refresh() { keyword.value = ''; page.value = 0; loadData() }
function goPage(p) { page.value = p; loadData() }
function openForm(student) {
editTarget.value = student || null
formVisible.value = true
}
async function handleFormSubmit(data) {
formLoading.value = true
try {
if (data.id) {
await updateStudent(data.id, data)
} else {
await createStudent(data)
}
formVisible.value = false
loadData()
} catch (e) {
alert(e.message)
} finally {
formLoading.value = false
}
}
async function handleDelete(id) {
if (!confirm('确认删除?')) return
try {
await deleteStudent(id)
loadData()
} catch (e) {
alert(e.message)
}
}
onMounted(loadData)
</script>
<style scoped>
.page { max-width: 1000px; margin: 30px auto; padding: 0 20px; }
/* 工具栏 */
.toolbar { display: flex; gap: 10px; margin-bottom: 16px; background: #fff; padding: 14px 18px; border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
.toolbar input { flex: 1; padding: 9px 12px; border: 1.5px solid #ddd; border-radius: 8px; font-size: 0.93em; }
.toolbar input:focus { outline: none; border-color: #667eea; }
.btn-outline { padding: 9px 18px; border: 1.5px solid #667eea; background: #fff; color: #667eea; border-radius: 8px; cursor: pointer; font-size: 0.9em; }
.btn-outline:hover { background: #f0f2ff; }
.btn-add { padding: 9px 22px; background: #667eea; color: #fff; border: none; border-radius: 8px; cursor: pointer; font-size: 0.9em; }
.btn-add:hover { background: #5a6fd6; }
/* 错误/加载/空状态 */
.error-bar { background: #fff3cd; color: #856404; padding: 12px 18px; border-radius: 8px; margin-bottom: 14px; display: flex; justify-content: space-between; }
.error-bar button { background: none; border: none; color: #856404; text-decoration: underline; cursor: pointer; }
.loading, .empty { text-align: center; padding: 80px 0; color: #fff; font-size: 1.1em; }
/* 表格 */
.table-wrap { background: #fff; border-radius: 10px; padding: 0 20px 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 0.93em; }
thead th { text-align: left; padding: 14px 10px; border-bottom: 2px solid #667eea; color: #667eea; font-weight: 600; white-space: nowrap; }
tbody td { padding: 12px 10px; border-bottom: 1px solid #f0f0f0; }
tbody tr:hover { background: #f8f9ff; }
/* 成绩徽章 */
.badge { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 0.82em; font-weight: 600; color: #fff; }
.badge-excellent { background: #2ecc71; }
.badge-good { background: #3498db; }
.badge-normal { background: #f39c12; }
.badge-poor { background: #e74c3c; }
/* 操作按钮 */
.btn-edit { padding: 4px 12px; background: #2ecc71; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 0.82em; margin-right: 6px; }
.btn-edit:hover { background: #27ae60; }
.btn-del { padding: 4px 12px; background: #e74c3c; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 0.82em; }
.btn-del:hover { background: #c0392b; }
/* 统计 */
.stats { background: #fff; border-radius: 10px; padding: 16px 20px; display: flex; gap: 14px; margin-top: 16px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
.stat { flex: 1; text-align: center; padding: 14px 10px; border-radius: 8px; background: linear-gradient(135deg, #667eea, #764ba2); }
.v { display: block; font-size: 1.5em; font-weight: 700; color: #fff; }
.l { display: block; font-size: 0.78em; color: rgba(255,255,255,0.8); margin-top: 3px; }
@media (max-width: 600px) {
.toolbar { flex-wrap: wrap; }
.stats { flex-direction: column; }
}
</style>