Files
2026-09-14 10:23:36 +08:00

276 lines
12 KiB
JavaScript
Raw Permalink 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.
/* 学生管理:增删改查 + 按学号/姓名/班级/顾问筛选
注意:学号是后端按班级开课时间生成的,表单里不给填,新增成功后提示出来 */
window.PAGES.push({
key: 'students',
title: '学生管理',
sub: '学生基本信息,学号由班级开课时间自动生成',
group: '数据管理',
icon: 'student',
component: {
name: 'StudentsPage',
template: `
<div>
<div class="card">
<div class="filters">
<el-input v-model="filter.stu_no" placeholder="学号(模糊)" clearable style="width:150px" @keyup.enter="load" />
<el-input v-model="filter.stu_name" placeholder="姓名(模糊)" clearable style="width:150px" @keyup.enter="load" />
<el-select v-model="filter.class_id" placeholder="全部班级" clearable style="width:210px">
<el-option v-for="c in classes" :key="c.class_id" :label="classLabel(c.class_id)" :value="c.class_id" />
</el-select>
<el-select v-model="filter.advisor_id" placeholder="全部顾问" clearable style="width:160px">
<el-option v-for="a in advisors" :key="a.adv_id" :label="a.adv_name" :value="a.adv_id" />
</el-select>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</div>
<div class="card-head">
<div>
<div class="card-title">学生列表</div>
<div class="card-hint">共 {{ rows.length }} 人</div>
</div>
<div class="card-tools">
<el-button type="primary" @click="openCreate">+ 新增学生</el-button>
</div>
</div>
<el-table :data="pagedRows" v-loading="loading" border stripe row-key="stu_no"
:header-cell-style="{ background: '#F8FAFC' }">
<el-table-column prop="stu_no" label="学号" width="120" fixed />
<el-table-column prop="stu_name" label="姓名" width="100" />
<el-table-column label="班级" width="180" show-overflow-tooltip>
<template #default="{ row }">{{ className(row.class_id) }}</template>
</el-table-column>
<el-table-column label="顾问" width="100">
<template #default="{ row }">{{ advisorName(row.advisor_id) }}</template>
</el-table-column>
<el-table-column label="性别" width="70">
<template #default="{ row }">
<el-tag v-if="row.stu_gender" size="small"
:type="row.stu_gender === '男' ? 'primary' : 'danger'" effect="light">
{{ row.stu_gender }}
</el-tag>
<span v-else class="muted">—</span>
</template>
</el-table-column>
<el-table-column prop="stu_age" label="年龄" width="70">
<template #default="{ row }">{{ dash(row.stu_age) }}</template>
</el-table-column>
<el-table-column prop="native_place" label="籍贯" width="100">
<template #default="{ row }">{{ dash(row.native_place) }}</template>
</el-table-column>
<el-table-column prop="graduate_school" label="毕业学校" min-width="140" show-overflow-tooltip>
<template #default="{ row }">{{ dash(row.graduate_school) }}</template>
</el-table-column>
<el-table-column prop="major" label="专业" width="120" show-overflow-tooltip>
<template #default="{ row }">{{ dash(row.major) }}</template>
</el-table-column>
<el-table-column prop="edu" label="学历" width="80">
<template #default="{ row }">{{ dash(row.edu) }}</template>
</el-table-column>
<el-table-column label="入学时间" width="110">
<template #default="{ row }">{{ dash(fmtDate(row.in_time)) }}</template>
</el-table-column>
<el-table-column label="毕业时间" width="110">
<template #default="{ row }">{{ dash(fmtDate(row.out_time)) }}</template>
</el-table-column>
<el-table-column label="操作" width="140" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" @click="remove(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination background layout="total, sizes, prev, pager, next"
:total="rows.length" v-model:current-page="page"
v-model:page-size="pageSize" :page-sizes="[10, 20, 50, 100]" />
</div>
</div>
<el-dialog v-model="dialog" :title="editing ? '编辑学生' : '新增学生'" width="720px" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="rules" label-width="88px">
<div class="form-grid">
<el-form-item label="班级" prop="class_id" class="span2">
<el-select v-model="form.class_id" placeholder="请选择班级(学号按开班时间生成)" style="width:100%">
<el-option v-for="c in classes" :key="c.class_id" :label="classLabel(c.class_id)" :value="c.class_id" />
</el-select>
</el-form-item>
<el-form-item label="姓名" prop="stu_name">
<el-input v-model="form.stu_name" placeholder="学生姓名" maxlength="30" />
</el-form-item>
<el-form-item label="顾问">
<el-select v-model="form.advisor_id" placeholder="可不选" clearable style="width:100%">
<el-option v-for="a in advisors" :key="a.adv_id" :label="a.adv_name" :value="a.adv_id" />
</el-select>
</el-form-item>
<el-form-item label="性别">
<el-select v-model="form.stu_gender" placeholder="可不选" clearable style="width:100%">
<el-option v-for="g in GENDER_OPTIONS" :key="g" :label="g" :value="g" />
</el-select>
</el-form-item>
<el-form-item label="年龄">
<el-input-number v-model="form.stu_age" :min="0" :max="100" controls-position="right" style="width:100%" />
</el-form-item>
<el-form-item label="籍贯">
<el-input v-model="form.native_place" placeholder="如 广东 深圳" maxlength="50" />
</el-form-item>
<el-form-item label="学历">
<el-select v-model="form.edu" placeholder="可不选" clearable filterable allow-create style="width:100%">
<el-option v-for="e in EDU_OPTIONS" :key="e" :label="e" :value="e" />
</el-select>
</el-form-item>
<el-form-item label="毕业学校" class="span2">
<el-input v-model="form.graduate_school" placeholder="毕业学校" maxlength="128" />
</el-form-item>
<el-form-item label="专业" class="span2">
<el-input v-model="form.major" placeholder="所学专业" maxlength="64" />
</el-form-item>
<el-form-item label="入学时间">
<el-date-picker v-model="form.in_time" type="date" value-format="YYYY-MM-DD"
placeholder="选择日期" style="width:100%" />
</el-form-item>
<el-form-item label="毕业时间">
<el-date-picker v-model="form.out_time" type="date" value-format="YYYY-MM-DD"
placeholder="选择日期" style="width:100%" />
</el-form-item>
</div>
<el-alert v-if="!editing" type="info" :closable="false" show-icon
title="学号不需要填写:后端会按所选班级的开课时间生成" />
</el-form>
<template #footer>
<el-button @click="dialog = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
</template>
</el-dialog>
</div>
`,
setup() {
const loading = ref(false);
const saving = ref(false);
const rows = ref([]);
const classes = ref([]);
const advisors = ref([]);
const dialog = ref(false);
const editing = ref(null);
const formRef = ref(null);
const filter = ref({ stu_no: '', stu_name: '', class_id: null, advisor_id: null });
const page = ref(1);
const pageSize = ref(10);
const emptyForm = () => ({
class_id: null, advisor_id: null, stu_name: '', native_place: '',
graduate_school: '', major: '', in_time: null, out_time: null,
edu: null, stu_age: null, stu_gender: null,
});
const form = ref(emptyForm());
// 学号是后端生成的,所以班级必选;其余字段都可空
const rules = {
class_id: [{ required: true, message: '请选择班级', trigger: 'change' }],
stu_name: [{ required: true, message: '请填写姓名', trigger: 'blur' }],
};
// 表格是客户端分页:接口本身没有分页参数,一次把命中的都取回来
const pagedRows = computed(() => {
const start = (page.value - 1) * pageSize.value;
return rows.value.slice(start, start + pageSize.value);
});
const load = async () => {
loading.value = true;
try {
rows.value = await StudentApi.list({
stu_no: filter.value.stu_no,
stu_name: filter.value.stu_name,
class_id: filter.value.class_id,
advisor_id: filter.value.advisor_id,
});
page.value = 1;
} catch (e) { toastErr(e); }
finally { loading.value = false; }
};
const reset = () => {
filter.value = { stu_no: '', stu_name: '', class_id: null, advisor_id: null };
load();
};
const openCreate = () => {
editing.value = null;
form.value = emptyForm();
dialog.value = true;
};
const openEdit = (row) => {
editing.value = row.stu_no;
form.value = {
class_id: row.class_id, advisor_id: row.advisor_id, stu_name: row.stu_name,
native_place: row.native_place, graduate_school: row.graduate_school,
major: row.major, in_time: fmtDate(row.in_time) || null,
out_time: fmtDate(row.out_time) || null, edu: row.edu,
stu_age: row.stu_age, stu_gender: row.stu_gender,
};
dialog.value = true;
};
const submit = async () => {
if (!formRef.value) return;
try { await formRef.value.validate(); } catch (e) { return; }
saving.value = true;
try {
// pruneEmpty:没填的字段不下发,避免把可空字段写成空字符串
const payload = pruneEmpty(form.value);
if (editing.value) {
await StudentApi.update(editing.value, payload);
toastOk('修改成功');
} else {
const created = await StudentApi.create(payload);
toastOk('新增成功,学号:' + created.stu_no);
}
Cache.invalidate('students');
dialog.value = false;
await load();
} catch (e) { toastErr(e); }
finally { saving.value = false; }
};
const remove = async (row) => {
try {
await ElementPlus.ElMessageBox.confirm(
`确定删除学生「${row.stu_name || row.stu_no}」吗?`,
'删除确认', { type: 'warning', confirmButtonText: '确定删除', cancelButtonText: '取消' });
} catch (e) { return; }
try {
await StudentApi.remove(row.stu_no);
toastOk('删除成功');
Cache.invalidate('students');
await load();
} catch (e) { toastErr(e); }
};
// 班级/顾问下拉的数据先准备好,再拉学生列表
onMounted(async () => {
try {
[classes.value, advisors.value] = await Promise.all([
Cache.loadClasses(), Cache.loadAdvisors(),
]);
} catch (e) { toastErr(e); }
load();
});
return {
loading, saving, rows, classes, advisors, dialog, editing, formRef,
filter, form, rules, page, pageSize, pagedRows,
load, reset, openCreate, openEdit, submit, remove,
GENDER_OPTIONS, EDU_OPTIONS, dash, fmtDate, className, advisorName, classLabel,
};
},
},
});