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

156 lines
6.7 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.
/* 唯一的网络出口。页面代码里不出现 fetch,所有接口怪癖都在这一层消化掉。
前端和后端不同源(前端在 static 目录下单独用 python -m http.server 起),
所以这里必须写全后端地址;后端 main.py 里配了 CORS 中间件放行。 */
const API_BASE = 'http://127.0.0.1:8001';
class ApiError extends Error {
constructor(message, status, body) {
super(message);
this.name = 'ApiError';
this.status = status;
this.body = body;
}
}
// FastAPI 的错误统一放在 {"detail": ...};422 的 detail 是数组,拼成一句中文
function detailOf(data) {
if (!data || data.detail === undefined || data.detail === null) return null;
const d = data.detail;
if (typeof d === 'string') return d;
if (Array.isArray(d)) {
return d.map(x => {
const where = (x.loc || []).slice(1).join('.') || '参数';
return `${where}:${x.msg}`;
}).join(';');
}
return JSON.stringify(d);
}
/**
* @param {string} method
* @param {string} path 必须写成后端的规范路径(尾斜杠与否由这里一处决定,
* 写错会吃 307 重定向,所以每个方法里都写死)
* @param {object} opts { query: 查询参数对象, body: JSON 请求体 }
* body 不传就不带请求体(成绩模块那几个接口全靠这个)
*/
async function request(method, path, opts = {}) {
let url = API_BASE + path;
if (opts.query) {
const usp = new URLSearchParams();
Object.entries(opts.query).forEach(([k, v]) => {
if (v !== undefined && v !== null && v !== '') usp.append(k, v);
});
const qs = usp.toString();
if (qs) url += '?' + qs;
}
const init = { method, headers: {} };
if (opts.body !== undefined) {
init.headers['Content-Type'] = 'application/json';
init.body = JSON.stringify(opts.body);
}
let res;
try {
res = await fetch(url, init);
} catch (e) {
throw new ApiError('无法连接后端服务,请确认 python main.py 已启动', 0, null);
}
const text = await res.text();
let data = null;
if (text) {
try { data = JSON.parse(text); } catch (e) {
// 正常走不到这里:路径写错时后端返回的是 JSON 404。多半是 API_BASE 配错了,
// 请求打到前端自己的 8080 上,拿回 http.server 的一页 HTML。
throw new ApiError('接口返回了非 JSON 内容,请检查 API_BASE 是否指向后端(HTTP ' + res.status + ')', res.status, null);
}
}
if (!res.ok) throw new ApiError(detailOf(data) || `请求失败(HTTP ${res.status})`, res.status, data);
return data;
}
/* ---------- 学生 ---------- 注意:路径是 "",所以没有尾斜杠 */
const StudentApi = {
list: (q) => request('GET', '/students', { query: q }),
create: (b) => request('POST', '/students', { body: b }), // 200(不是 201)
update: (no, b) => request('PUT', `/students/${encodeURIComponent(no)}`, { body: b }),
remove: (no) => request('DELETE', `/students/${encodeURIComponent(no)}`),
};
/* ---------- 班级 ---------- 集合路径带尾斜杠 */
const ClassApi = {
// 只有这个列表接口会带上 head_teacher_name / course_teacher_names
list: (q) => request('GET', '/classes/', { query: q }),
create: (b) => request('POST', '/classes/', { body: b }),
detail: (id) => request('GET', `/classes/${id}`), // 别用它取班主任名,永远是"暂无"
update: (id, b) => request('PUT', `/classes/${id}`, { body: b }),
async remove(id) {
// 这个接口删除失败也返回 HTTP 200,只是 body 里 code=404,必须看 body 而不是状态码
const r = await request('DELETE', `/classes/${id}`);
if (r && typeof r.code === 'number' && r.code !== 200) {
throw new ApiError(r.msg || '删除失败', 200, r);
}
return r;
},
};
/* ---------- 老师 ---------- 单数前缀 */
const TeacherApi = {
list: () => request('GET', '/teacher/'),
generateNo: () => request('GET', '/teacher/generate-no'), // -> { t_no }
create: (b) => request('POST', '/teacher/', { body: b }), // 201
update: (id, b) => request('PUT', `/teacher/${id}`, { body: b }),
async remove(id) {
// 老师不存在时返回 HTTP 200 + body 为 null,不抛 404,得自己判
const r = await request('DELETE', `/teacher/${id}`);
if (r === null) throw new ApiError('老师不存在', 200, null);
return r;
},
};
/* ---------- 顾问 ---------- */
const AdvisorApi = {
list: (q) => request('GET', '/advisor/', { query: q }),
create: (b) => request('POST', '/advisor/', { body: b }), // 201
update: (id, b) => request('PUT', `/advisor/${id}`, { body: b }),
remove: (id) => request('DELETE', `/advisor/${id}`),
};
/* ---------- 就业 ---------- 按学号定位 */
const EmpApi = {
list: (q) => request('GET', '/emp/', { query: q }),
create: (b) => request('POST', '/emp/', { body: b }), // 201,学号重复会 400
update: (no, b) => request('PUT', `/emp/${encodeURIComponent(no)}`, { body: b }),
async remove(no) {
const r = await request('DELETE', `/emp/${encodeURIComponent(no)}`);
if (r === null) throw new ApiError('就业信息不存在', 200, null);
return r;
},
};
/* ---------- 成绩 ----------
五个接口的标量参数都走 query string,没有 JSON body;
对外一律用业务学号 stu_no,不是内部主键 stu_id。 */
const ScoreApi = {
listByStuNo: (no) => request('GET', `/score/list/${encodeURIComponent(no)}`),
add: (no, order, score) => request('POST', '/score/add', { query: { stu_no: no, exam_order: order, score } }),
update: (no, order, newScore) => request('PUT', '/score/update', { query: { stu_no: no, exam_order: order, new_score: newScore } }),
del: (no, order) => request('DELETE', '/score/del', { query: { stu_no: no, exam_order: order } }),
};
/* ---------- 统计 ---------- */
const StatsApi = {
studentsByAge: (op, value, value2) => request('GET', '/statistics/students/age', { query: { op, value, value2 } }),
classCount: () => request('GET', '/statistics/classes/count'),
outScoreLine: (line) => request('GET', `/statistics/scores/out_score_line/${line}`),
failScore: (n) => request('GET', `/statistics/scores/fail_score/${n}`),
classAvgScore: (sortOrder) => request('GET', '/statistics/scores/get_class_avg_score', { query: { sort_order: sortOrder } }),
topRank: (rank) => request('GET', `/statistics/emp/top_rank/${rank}`),
empTime: () => request('GET', '/statistics/emp/emp_time'),
classAvgEmpTime: (sortOrder) => request('GET', '/statistics/emp/class_avg_emp_time', { query: { sort_order: sortOrder } }),
};