209 lines
7.7 KiB
Vue
209 lines
7.7 KiB
Vue
<!-- 学生列表页(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>
|