数据库脚本修复

This commit is contained in:
biss committed 2026-08-23 09:11:18 +08:00
1 parent b7b7c25d78
commit 14af0910e7
7 files changed
+500 -1

No files matched your search

+2
View File
@@ -68,6 +68,8 @@ export function apiErrorMessage(error: unknown) {
if (!axios.isAxiosError(error)) return '操作失败,请稍后重试。'
const status = error.response?.status
const data = error.response?.data
const message = typeof data?.message === 'string' ? data.message.trim() : ''
if (message) return message
if (data?.errors && typeof data.errors === 'object') {
const validationMessage = Object.values(data.errors)
.flat()
+3 -1
View File
@@ -4,7 +4,9 @@ import { ElMessageBox } from 'element-plus'
let updatePromptVisible = false
function serviceWorkerUrl() {
return new URL('sw.js', import.meta.env.BASE_URL).toString()
// Vite's BASE_URL is normally a path such as "/" rather than an absolute
// URL. The URL constructor requires an absolute base in browsers.
return new URL(`${import.meta.env.BASE_URL}sw.js`, window.location.origin).toString()
}
async function promptToRefresh(registration: ServiceWorkerRegistration) {
@@ -0,0 +1,97 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import http, { apiErrorMessage } from '../api/http'
import { downloadApiFile } from '../api/excel'
interface WalletStudent { id: string; studentNumber: string; name: string; collegeName: string; majorName: string; className: string; balance: number; lastChangedAt?: string }
interface Option { id: string; name: string }
interface MajorOption extends Option { collegeId: string }
interface ClassOption extends Option { grade: number; majorId: string }
const items = ref<WalletStudent[]>([])
const total = ref(0)
const page = ref(1)
const keyword = ref('')
const loading = ref(false)
const fileInput = ref<HTMLInputElement | null>(null)
const pageSize = 20
const colleges = ref<Option[]>([])
const majors = ref<MajorOption[]>([])
const classes = ref<ClassOption[]>([])
const filters = ref({ grade: undefined as number | undefined, collegeId: '', majorId: '', administrativeClassId: '' })
const filteredMajors = computed(() => majors.value.filter(x => !filters.value.collegeId || x.collegeId === filters.value.collegeId))
const filteredClasses = computed(() => classes.value.filter(x => (!filters.value.majorId || x.majorId === filters.value.majorId) && (!filters.value.grade || x.grade === filters.value.grade)))
const currency = (value: number) => `¥${value.toFixed(2)}`
const walletStudent = (value: unknown) => value as WalletStudent
const formatTime = (value?: string) => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '从未调整'
async function load() {
loading.value = true
try {
const { data } = await http.get('/campus-registrations/wallets/students', { params: { keyword: keyword.value || undefined, ...filters.value, page: page.value, pageSize } })
items.value = data.items; total.value = data.total
} catch (error) { ElMessage.error(apiErrorMessage(error)) }
finally { loading.value = false }
}
function resetDependent(level: 'college' | 'major' | 'grade') {
if (level === 'college') { filters.value.majorId = ''; filters.value.administrativeClassId = '' }
if (level === 'major' || level === 'grade') filters.value.administrativeClassId = ''
}
async function loadOptions() {
try { const { data } = await http.get('/campus-registrations/wallets/filter-options'); colleges.value = data.colleges; majors.value = data.majors; classes.value = data.classes }
catch (error) { ElMessage.error(apiErrorMessage(error)) }
}
async function adjust(item: WalletStudent) {
try {
const { value: amount } = await ElMessageBox.prompt(`为 ${item.name}(${item.studentNumber})调整余额;正数充值,负数扣减。`, '调整校园余额', { inputPattern: /^-?\d+(\.\d{1,2})?$/, inputErrorMessage: '请输入金额,最多两位小数' })
const { value: reason } = await ElMessageBox.prompt('请填写调整原因。该内容将永久保留在余额流水中。', '调整原因', { inputValidator: value => value.trim() ? true : '必须填写调整原因' })
await http.post(`/campus-registrations/wallets/students/${item.id}/adjust`, { amount: Number(amount), reason })
ElMessage.success('余额已调整'); await load()
} catch (error) { if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error)) }
}
async function downloadTemplate() {
try { await downloadApiFile('/campus-registrations/wallets/adjustment-template.xlsx', '学生余额批量调整模板.xlsx') }
catch (error) { ElMessage.error(apiErrorMessage(error)) }
}
async function exportWallets() {
try { await downloadApiFile('/campus-registrations/wallets/export.xlsx', '学生余额.xlsx') }
catch (error) { ElMessage.error(apiErrorMessage(error)) }
}
async function importFile(event: Event) {
const file = (event.target as HTMLInputElement).files?.[0]; if (!file) return
const form = new FormData(); form.append('file', file)
try { const { data } = await http.post('/campus-registrations/wallets/import-adjustments', form); ElMessage.success(`已调整 ${data.adjusted} 名学生`); await load() }
catch (error) { ElMessage.error(apiErrorMessage(error)) }
finally { if (fileInput.value) fileInput.value.value = '' }
}
onMounted(async () => { await loadOptions(); await load() })
</script>
<template>
<section class="wallet-page" v-loading="loading">
<header><p>FINANCIAL LEDGER</p><h2>学生余额管理</h2><span>独立资金台账,覆盖全部学生;不使用学生档案管理页面。</span></header>
<el-card>
<div class="toolbar">
<el-input v-model="keyword" clearable placeholder="按学号或姓名搜索" @keyup.enter="page = 1; load()" />
<el-select v-model="filters.grade" clearable placeholder="年级" @change="resetDependent('grade')"><el-option v-for="value in [...new Set(classes.map(x => x.grade))].sort()" :key="value" :label="`${value} 级`" :value="value" /></el-select>
<el-select v-model="filters.collegeId" clearable placeholder="学院" @change="resetDependent('college')"><el-option v-for="item in colleges" :key="item.id" :label="item.name" :value="item.id" /></el-select>
<el-select v-model="filters.majorId" clearable placeholder="专业" @change="resetDependent('major')"><el-option v-for="item in filteredMajors" :key="item.id" :label="item.name" :value="item.id" /></el-select>
<el-select v-model="filters.administrativeClassId" clearable placeholder="行政班"><el-option v-for="item in filteredClasses" :key="item.id" :label="`${item.grade}级 ${item.name}`" :value="item.id" /></el-select>
<el-button type="primary" @click="page = 1; load()">查询</el-button>
<el-button @click="downloadTemplate">下载批量模板</el-button>
<el-button @click="fileInput?.click()">导入批量调整</el-button>
<el-button @click="exportWallets">导出当前学生余额</el-button>
<input ref="fileInput" type="file" accept=".xlsx" hidden @change="importFile" />
</div>
<el-alert type="info" :closable="false" title="批量模板须填写学号、调整金额与调整原因;正数充值,负数扣减。导出包含当前筛选范围内全部学生。" />
<el-table :data="items" style="margin-top:16px"><el-table-column label="学生" min-width="210"><template #default="{ row }"><b>{{ walletStudent(row).studentNumber }} · {{ walletStudent(row).name }}</b><small>{{ walletStudent(row).collegeName }} · {{ walletStudent(row).majorName }} · {{ walletStudent(row).className }}</small></template></el-table-column><el-table-column label="当前余额" width="150"><template #default="{ row }"><strong>{{ currency(walletStudent(row).balance) }}</strong></template></el-table-column><el-table-column label="最后变动" width="190"><template #default="{ row }">{{ formatTime(walletStudent(row).lastChangedAt) }}</template></el-table-column><el-table-column label="操作" width="120" fixed="right"><template #default="{ row }"><el-button type="primary" link @click="adjust(walletStudent(row))">调整余额</el-button></template></el-table-column></el-table>
<el-empty v-if="items.length === 0" description="没有符合条件的学生" />
<el-pagination v-if="total > pageSize" v-model:current-page="page" :total="total" :page-size="pageSize" layout="prev, pager, next" @current-change="load" />
</el-card>
</section>
</template>
<style scoped>
.wallet-page{display:grid;gap:20px}.wallet-page header p{margin:0;color:#17867c;font-size:12px;font-weight:800;letter-spacing:.16em}.wallet-page h2{margin:6px 0;color:#183b56}.wallet-page header span,small{color:#738596}.toolbar{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:16px}.toolbar .el-input{max-width:260px}.toolbar .el-select{width:150px}.el-table b,.el-table small{display:block}.el-table strong{color:#17867c;font-size:16px}.el-pagination{justify-content:flex-end;margin-top:16px}@media(max-width:720px){.toolbar{flex-direction:column}.toolbar .el-input,.toolbar .el-select,.toolbar .el-button{max-width:none;width:100%}}
</style>
Loaded 3 of 7 files, more files were not shown because too many files have changed in this diff. Show more