Week 1-8: Spring Boot 学习计划完整项目
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
208
week7/frontend/src/views/StudentListView.vue
Normal file
208
week7/frontend/src/views/StudentListView.vue
Normal file
@@ -0,0 +1,208 @@
|
||||
<!-- 学生列表页(Day 3-6:CRUD + 分页 + 搜索 + 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>
|
||||
Reference in New Issue
Block a user