156 lines
6.7 KiB
JavaScript
156 lines
6.7 KiB
JavaScript
/* 唯一的网络出口。页面代码里不出现 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 } }),
|
|||
|
|
};
|