317 lines
20 KiB
HTML
317 lines
20 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>沃林学生管理系统</title>
|
||
<link rel="stylesheet" href="/static/style.css" />
|
||
<script src="https://cdn.jsdelivr.net/npm/vue@3.4.27/dist/vue.global.prod.js"></script>
|
||
</head>
|
||
<body>
|
||
<div id="app">
|
||
<!-- ==================== 登录页 ==================== -->
|
||
<div v-if="!token" class="login-wrap">
|
||
<div class="login-card">
|
||
<h2>沃林学生管理系统</h2>
|
||
<p class="sub">FastAPI + MySQL 管理后台</p>
|
||
<label>用户名</label>
|
||
<input v-model.trim="loginForm.username" @keyup.enter="login" placeholder="请输入用户名" />
|
||
<label>密码</label>
|
||
<input v-model="loginForm.password" type="password" @keyup.enter="login" placeholder="请输入密码" />
|
||
<button class="btn primary block" :disabled="loading" @click="login">{{ loading ? '登录中...' : '登 录' }}</button>
|
||
<p class="tip">{{ tip }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ==================== 主界面 ==================== -->
|
||
<template v-else>
|
||
<header class="topbar">
|
||
<span class="logo">沃林学生管理系统</span>
|
||
<span class="spacer"></span>
|
||
<span class="role-tag">{{ roleLabel }}</span>
|
||
<span>{{ me.username }}</span>
|
||
<button class="btn ghost" @click="logout">退出登录</button>
|
||
</header>
|
||
|
||
<nav class="tabs">
|
||
<button v-for="t in tabs" :key="t.key" :class="['tab', { active: tab === t.key }]" @click="switchTab(t.key)">{{ t.label }}</button>
|
||
</nav>
|
||
|
||
<main class="content">
|
||
<!-- ============ 学生管理 ============ -->
|
||
<section v-show="tab === 'students'">
|
||
<div class="toolbar">
|
||
<input v-model.number="stuQ.stu_id" placeholder="学号" class="w90" />
|
||
<input v-model.trim="stuQ.stu_name" placeholder="姓名(模糊)" class="w120" />
|
||
<select v-model="stuQ.class_id"><option value="">全部班级</option><option v-for="c in classes" :key="c.class_id" :value="c.class_id">{{ c.class_name }}</option></select>
|
||
<select v-model="stuQ.status"><option value="">全部状态</option><option>在读</option><option>进入就业</option><option>已就业</option></select>
|
||
<button class="btn" @click="loadStudents(1)">查询</button>
|
||
<button class="btn ghost" @click="resetStuQ">重置</button>
|
||
<span class="spacer"></span>
|
||
<button v-if="me.role !== 'student'" class="btn primary" @click="openStuForm()">+ 新增学生</button>
|
||
</div>
|
||
<table>
|
||
<thead><tr><th>学号</th><th>姓名</th><th>班级</th><th>性别</th><th>年龄</th><th>学历</th><th>专业</th><th>籍贯</th><th>毕业院校</th><th>入学</th><th>状态</th><th v-if="me.role!=='student'">操作</th></tr></thead>
|
||
<tbody>
|
||
<tr v-for="s in students" :key="s.stu_id">
|
||
<td>{{ s.stu_id }}</td><td>{{ s.stu_name }}</td><td>{{ s.class_name }}</td><td>{{ s.gender }}</td><td>{{ s.age }}</td>
|
||
<td>{{ s.education }}</td><td>{{ s.major }}</td><td>{{ s.native_place }}</td><td>{{ s.graduate_school }}</td>
|
||
<td>{{ s.enroll_time }}</td><td><span :class="['badge', s.status==='已就业'?'ok':(s.status==='进入就业'?'warn':'')]">{{ s.status }}</span></td>
|
||
<td v-if="me.role!=='student'">
|
||
<button class="btn mini" @click="openStuForm(s)">编辑</button>
|
||
<button class="btn mini danger" @click="delStudent(s)">删除</button>
|
||
</td>
|
||
</tr>
|
||
<tr v-if="!students.length"><td colspan="12" class="empty">暂无数据</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<pager :page="stuPage" :total="stuTotal" @go="loadStudents"></pager>
|
||
</section>
|
||
|
||
<!-- ============ 成绩管理 ============ -->
|
||
<section v-show="tab === 'scores'">
|
||
<div class="toolbar">
|
||
<input v-model.number="scoreQ.stu_id" placeholder="学号" class="w90" />
|
||
<input v-model.number="scoreQ.exam_id" placeholder="考核序次" class="w90" />
|
||
<button class="btn" @click="loadScores">查询</button>
|
||
<span class="spacer"></span>
|
||
<button v-if="me.role !== 'student'" class="btn primary" @click="openScoreForm()">+ 录入成绩</button>
|
||
</div>
|
||
<p class="warn-tip" v-if="scoreWarning">{{ scoreWarning }}</p>
|
||
<table>
|
||
<thead><tr><th>学号</th><th>姓名</th><th>考核序次</th><th>成绩</th><th>预警</th><th v-if="me.role!=='student'">操作</th></tr></thead>
|
||
<tbody>
|
||
<tr v-for="s in scores" :key="s.stu_id + '-' + s.exam_id">
|
||
<td>{{ s.stu_id }}</td><td>{{ s.stu_name }}</td><td>第 {{ s.exam_id }} 次</td><td>{{ s.score }}</td>
|
||
<td><span v-if="s.is_warning" class="badge danger">低于60分</span><span v-else class="badge ok">正常</span></td>
|
||
<td v-if="me.role!=='student'">
|
||
<button class="btn mini" @click="openScoreForm(s)">编辑</button>
|
||
<button class="btn mini danger" @click="delScore(s)">删除</button>
|
||
</td>
|
||
</tr>
|
||
<tr v-if="!scores.length"><td colspan="6" class="empty">暂无数据</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</section>
|
||
|
||
<!-- ============ 就业管理 ============ -->
|
||
<section v-show="tab === 'employment'">
|
||
<div class="toolbar">
|
||
<input v-model.number="empQ.stu_id" placeholder="学号" class="w90" />
|
||
<input v-model.trim="empQ.company_name" placeholder="公司名称(模糊)" class="w140" />
|
||
<input v-model.number="empQ.salary_min" placeholder="薪资下限" class="w90" />
|
||
<input v-model.number="empQ.salary_max" placeholder="薪资上限" class="w90" />
|
||
<button class="btn" @click="loadEmployment(1)">查询</button>
|
||
<span class="spacer"></span>
|
||
<button v-if="me.role !== 'student'" class="btn primary" @click="empFormMode='open'; showEmp=true">+ 就业开放</button>
|
||
<button v-if="me.role !== 'student'" class="btn primary" @click="empFormMode='offer'; showEmp=true">+ 登记 offer</button>
|
||
</div>
|
||
<table>
|
||
<thead><tr><th>学号</th><th>姓名</th><th>班级</th><th>开放时间</th><th>offer时间</th><th>公司</th><th>薪资</th><th v-if="me.role!=='student'">操作</th></tr></thead>
|
||
<tbody>
|
||
<tr v-for="e in employment" :key="e.stu_id">
|
||
<td>{{ e.stu_id }}</td><td>{{ e.stu_name }}</td><td>{{ e.class_name }}</td>
|
||
<td>{{ e.employment_open_time }}</td><td>{{ e.job_time || '—' }}</td><td>{{ e.company_name }}</td><td>¥{{ e.salary }}</td>
|
||
<td v-if="me.role!=='student'">
|
||
<button class="btn mini" @click="openEmpEdit(e)">编辑</button>
|
||
<button class="btn mini danger" @click="delEmployment(e)">删除</button>
|
||
</td>
|
||
</tr>
|
||
<tr v-if="!employment.length"><td colspan="8" class="empty">暂无数据</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<pager :page="empPage" :total="empTotal" @go="loadEmployment"></pager>
|
||
</section>
|
||
|
||
<!-- ============ 班级管理 ============ -->
|
||
<section v-show="tab === 'classes'">
|
||
<div class="toolbar">
|
||
<span class="spacer"></span>
|
||
<button v-if="me.role === 'admin'" class="btn primary" @click="openClassForm()">+ 新增班级</button>
|
||
</div>
|
||
<table>
|
||
<thead><tr><th>班级ID</th><th>班级名称</th><th>开课时间</th><th v-if="me.role==='admin'">操作</th></tr></thead>
|
||
<tbody>
|
||
<tr v-for="c in classes" :key="c.class_id">
|
||
<td>{{ c.class_id }}</td><td>{{ c.class_name }}</td><td>{{ c.start_time }}</td>
|
||
<td v-if="me.role==='admin'">
|
||
<button class="btn mini" @click="openClassForm(c)">编辑</button>
|
||
<button class="btn mini danger" @click="delClass(c)">删除</button>
|
||
</td>
|
||
</tr>
|
||
<tr v-if="!classes.length"><td colspan="4" class="empty">暂无数据</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</section>
|
||
|
||
<!-- ============ 老师管理 ============ -->
|
||
<section v-show="tab === 'teachers'">
|
||
<div class="toolbar">
|
||
<span class="spacer"></span>
|
||
<button v-if="me.role === 'admin'" class="btn primary" @click="openTeacherForm()">+ 新增老师</button>
|
||
</div>
|
||
<table>
|
||
<thead><tr><th>工号</th><th>姓名</th><th>职务</th><th>带班</th><th v-if="me.role==='admin'">操作</th></tr></thead>
|
||
<tbody>
|
||
<tr v-for="t in teachers" :key="t.teacher_id">
|
||
<td>{{ t.teacher_id }}</td><td>{{ t.teacher_name }}</td><td>{{ t.job_name }}</td><td>{{ t.class_name }}</td>
|
||
<td v-if="me.role==='admin'">
|
||
<button class="btn mini" @click="openTeacherForm(t)">编辑</button>
|
||
<button class="btn mini danger" @click="delTeacher(t)">删除</button>
|
||
</td>
|
||
</tr>
|
||
<tr v-if="!teachers.length"><td colspan="5" class="empty">暂无数据</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<pager :page="teacherPage" :total="teacherTotal" @go="loadTeachers"></pager>
|
||
</section>
|
||
|
||
<!-- ============ 统计分析 ============ -->
|
||
<section v-show="tab === 'statistics'">
|
||
<div class="toolbar wrap">
|
||
<button class="btn" @click="runStat('byAge')">动态年龄查询</button>
|
||
<button class="btn" @click="runStat('genderStats')">班级性别统计</button>
|
||
<button class="btn" @click="runStat('allAbove')">分数线以上学生</button>
|
||
<button class="btn" @click="runStat('fail')">不及格>=N次</button>
|
||
<button class="btn" @click="runStat('classAvg')">班级考试平均分</button>
|
||
<button class="btn" @click="runStat('topSalary')">薪资TopN</button>
|
||
<button class="btn" @click="runStat('duration')">就业时长</button>
|
||
<button class="btn" @click="runStat('classAvgDuration')">班级平均就业时长</button>
|
||
<button class="btn" @click="runStat('volatility')">成绩波动Top5</button>
|
||
<button class="btn" @click="runStat('funnel')">班级就业漏斗</button>
|
||
</div>
|
||
|
||
<!-- 动态年龄查询参数 -->
|
||
<div class="param-box" v-if="statKey==='byAge'">
|
||
<select v-model="statParams.op"><option value="gt">大于</option><option value="lt">小于</option><option value="eq">等于</option><option value="gte">大于等于</option><option value="lte">小于等于</option><option value="between">区间</option></select>
|
||
<input v-model.number="statParams.value" placeholder="年龄" v-if="statParams.op!=='between'" class="w90" />
|
||
<input v-model.number="statParams.min_value" placeholder="最小" v-if="statParams.op==='between'" class="w70" />
|
||
<input v-model.number="statParams.max_value" placeholder="最大" v-if="statParams.op==='between'" class="w70" />
|
||
<button class="btn primary" @click="doStat">执行</button>
|
||
</div>
|
||
<div class="param-box" v-if="['allAbove','fail','topSalary','classAvg','volatility'].includes(statKey)">
|
||
<label v-if="statKey==='allAbove'">分数线 <input v-model.number="statParams.line" class="w70" /></label>
|
||
<label v-if="statKey==='fail'">不及格次数>= <input v-model.number="statParams.times" class="w70" /></label>
|
||
<label v-if="statKey==='topSalary'">Top <input v-model.number="statParams.n" class="w70" /></label>
|
||
<label v-if="statKey==='classAvg'">考核序次(可空) <input v-model.number="statParams.exam_id" class="w70" /></label>
|
||
<label v-if="statKey==='classAvg'">排序 <select v-model="statParams.order"><option value="desc">从高到低</option><option value="asc">从低到高</option></select></label>
|
||
<label v-if="statKey==='volatility'">Top <input v-model.number="statParams.top_n" class="w70" /></label>
|
||
<button class="btn primary" @click="doStat">执行</button>
|
||
</div>
|
||
|
||
<table v-if="statRows.length">
|
||
<thead><tr><th v-for="h in statHeaders" :key="h">{{ h }}</th></tr></thead>
|
||
<tbody>
|
||
<tr v-for="(r,i) in statRows" :key="i"><td v-for="h in statKeys" :key="h">{{ r[h] }}</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<p v-else class="empty">点击上方按钮执行统计查询</p>
|
||
</section>
|
||
|
||
<!-- ============ 高级筛选器 ============ -->
|
||
<section v-show="tab === 'filter'">
|
||
<p class="sub-tip">通用高级筛选器:对 student 模型进行 AND/OR 嵌套规则筛选。支持字段:stu_id, stu_name, age, gender, education, major, native_place, status, class_id, class_name, salary, company_name;操作符:> < = != >= <= like in</p>
|
||
<textarea class="filter-json" v-model="filterJson" rows="12"></textarea>
|
||
<div class="toolbar">
|
||
<button class="btn primary" @click="runFilter">执行筛选</button>
|
||
<button class="btn ghost" @click="resetFilterJson">恢复示例</button>
|
||
</div>
|
||
<table v-if="filterRows.length">
|
||
<thead><tr><th>学号</th><th>姓名</th><th>班级</th><th>性别</th><th>年龄</th><th>学历</th><th>状态</th><th>薪资</th><th>公司</th></tr></thead>
|
||
<tbody>
|
||
<tr v-for="s in filterRows" :key="s.stu_id">
|
||
<td>{{ s.stu_id }}</td><td>{{ s.stu_name }}</td><td>{{ s.class_name }}</td><td>{{ s.gender }}</td><td>{{ s.age }}</td><td>{{ s.education }}</td><td>{{ s.status }}</td>
|
||
<td>{{ s.salary ?? '—' }}</td><td>{{ s.company_name ?? '—' }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<p class="empty" v-else>输入规则 JSON 后执行筛选</p>
|
||
</section>
|
||
</main>
|
||
</template>
|
||
|
||
<!-- ==================== 弹窗表单 ==================== -->
|
||
<div class="modal-mask" v-if="showStu || showScore || showEmp || showClass || showTeacher" @click.self="closeAll">
|
||
<!-- 学生表单 -->
|
||
<div class="modal" v-if="showStu">
|
||
<h3>{{ stuForm.stu_id ? '编辑学生' : '新增学生' }}</h3>
|
||
<div class="form-grid">
|
||
<label>学号(留空自动生成)<input v-model.trim="stuForm.stu_id" placeholder="留空自动生成" :disabled="!!stuForm._editing" /></label>
|
||
<label>班级<select v-model.number="stuForm.class_id"><option v-for="c in classes" :key="c.class_id" :value="c.class_id">{{ c.class_id }} - {{ c.class_name }}</option></select></label>
|
||
<label>姓名<input v-model.trim="stuForm.stu_name" /></label>
|
||
<label>性别<select v-model="stuForm.gender"><option>男</option><option>女</option></select></label>
|
||
<label>年龄<input v-model.number="stuForm.age" type="number" /></label>
|
||
<label>学历<input v-model.trim="stuForm.education" placeholder="大专/本科/硕士" /></label>
|
||
<label>专业<input v-model.trim="stuForm.major" /></label>
|
||
<label>籍贯<input v-model.trim="stuForm.native_place" /></label>
|
||
<label>毕业院校<input v-model.trim="stuForm.graduate_school" /></label>
|
||
<label>顾问ID<input v-model.number="stuForm.advisor_id" type="number" :disabled="!!stuForm._editing" /></label>
|
||
<label>入学时间<input v-model.trim="stuForm.enroll_time" type="date" :disabled="!!stuForm._editing" /></label>
|
||
<label>毕业时间<input v-model.trim="stuForm.graduate_time" type="date" :disabled="!!stuForm._editing" /></label>
|
||
<label>状态<select v-model="stuForm.status"><option>在读</option><option>进入就业</option><option>已就业</option></select></label>
|
||
</div>
|
||
<div class="modal-actions"><button class="btn" @click="closeAll">取消</button><button class="btn primary" @click="saveStudent">保存</button></div>
|
||
</div>
|
||
|
||
<!-- 成绩表单 -->
|
||
<div class="modal" v-if="showScore">
|
||
<h3>{{ scoreForm._editing ? '修改成绩' : '录入成绩' }}</h3>
|
||
<div class="form-grid">
|
||
<label>学号<input v-model.number="scoreForm.stu_id" type="number" :disabled="!!scoreForm._editing" /></label>
|
||
<label>考核序次<input v-model.number="scoreForm.exam_id" type="number" :disabled="!!scoreForm._editing" /></label>
|
||
<label>成绩(0-100)<input v-model.number="scoreForm.score" type="number" min="0" max="100" /></label>
|
||
</div>
|
||
<div class="modal-actions"><button class="btn" @click="closeAll">取消</button><button class="btn primary" @click="saveScore">保存</button></div>
|
||
</div>
|
||
|
||
<!-- 就业表单 -->
|
||
<div class="modal" v-if="showEmp">
|
||
<h3>{{ empFormMode === 'open' ? '登记就业开放' : empFormMode === 'offer' ? '登记 offer' : '编辑就业信息' }}</h3>
|
||
<div class="form-grid">
|
||
<label>学号<input v-model.number="empForm.stu_id" type="number" /></label>
|
||
<template v-if="empFormMode === 'open'">
|
||
<label>就业开放时间<input v-model.trim="empForm.employment_open_time" type="date" /></label>
|
||
</template>
|
||
<template v-else-if="empFormMode === 'offer'">
|
||
<label>offer 下发时间<input v-model.trim="empForm.offer_time" type="date" /></label>
|
||
<label>公司名称<input v-model.trim="empForm.company_name" /></label>
|
||
<label>薪资<input v-model.number="empForm.salary" type="number" /></label>
|
||
</template>
|
||
<template v-else>
|
||
<label>公司名称<input v-model.trim="empForm.company_name" /></label>
|
||
<label>薪资<input v-model.number="empForm.salary" type="number" /></label>
|
||
</template>
|
||
</div>
|
||
<div class="modal-actions"><button class="btn" @click="closeAll">取消</button><button class="btn primary" @click="saveEmployment">保存</button></div>
|
||
</div>
|
||
|
||
<!-- 班级表单 -->
|
||
<div class="modal" v-if="showClass">
|
||
<h3>{{ classForm.class_id ? '编辑班级' : '新增班级' }}</h3>
|
||
<div class="form-grid">
|
||
<label>班级名称<input v-model.trim="classForm.class_name" /></label>
|
||
<label>开课时间<input v-model.trim="classForm.start_time" type="date" /></label>
|
||
</div>
|
||
<div class="modal-actions"><button class="btn" @click="closeAll">取消</button><button class="btn primary" @click="saveClass">保存</button></div>
|
||
</div>
|
||
|
||
<!-- 老师表单 -->
|
||
<div class="modal" v-if="showTeacher">
|
||
<h3>{{ teacherForm.teacher_id ? '编辑老师' : '新增老师' }}</h3>
|
||
<div class="form-grid">
|
||
<label>姓名<input v-model.trim="teacherForm.teacher_name" /></label>
|
||
<label>职务<select v-model="teacherForm.job_name"><option>主讲</option><option>班主任</option><option>助教</option></select></label>
|
||
<label>带班班级<select v-model.number="teacherForm.class_id"><option v-for="c in classes" :key="c.class_id" :value="c.class_id">{{ c.class_id }} - {{ c.class_name }}</option></select></label>
|
||
</div>
|
||
<div class="modal-actions"><button class="btn" @click="closeAll">取消</button><button class="btn primary" @click="saveTeacher">保存</button></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 全局提示 -->
|
||
<div class="toast" v-if="tip && token">{{ tip }}</div>
|
||
</div>
|
||
|
||
<script src="/static/app.js"></script>
|
||
</body>
|
||
</html>
|