feat:添加前端页面
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
/* 通用工具:格式化、选项常量、跨模块下拉数据缓存、图标
|
||||
所有页面共用,先于 api.js 和页面脚本加载 */
|
||||
|
||||
// 本文件是第一个加载的脚本,把 Vue 的常用 API 解构到全局,
|
||||
// 后面的 api.js / 各页面 / app.js 直接引用 ref、computed 这些名字即可。
|
||||
// (各文件都是普通 <script>,顶层 const 处于同一个全局作用域)
|
||||
const { createApp, ref, reactive, computed, watch, onMounted, onUnmounted, nextTick } = Vue;
|
||||
|
||||
// 页面在这里注册自己,app.js 最后加载时统一挂载(顺序即左侧菜单顺序)
|
||||
window.PAGES = window.PAGES || [];
|
||||
|
||||
/* ---------- 格式化 ---------- */
|
||||
|
||||
// 把空值统一显示成占位符,表格里不会出现难看的 null / 空单元格
|
||||
function dash(v) {
|
||||
return (v === null || v === undefined || v === '') ? '—' : v;
|
||||
}
|
||||
|
||||
// "2026-09-01T00:00:00" -> "2026-09-01"
|
||||
function fmtDate(s) {
|
||||
if (!s) return '';
|
||||
return String(s).slice(0, 10);
|
||||
}
|
||||
|
||||
// "2026-09-01T14:57:12" -> "2026-09-01 14:57"
|
||||
function fmtDateTime(s) {
|
||||
if (!s) return '';
|
||||
const t = String(s).replace('T', ' ');
|
||||
return t.length >= 16 ? t.slice(0, 16) : t;
|
||||
}
|
||||
|
||||
// 提交前去掉空值:没填的字段干脆不下发,
|
||||
// 这样后端的 exclude_none 和数据库默认值(如薪资 15000)才有机会生效
|
||||
function pruneEmpty(obj) {
|
||||
const out = {};
|
||||
Object.entries(obj || {}).forEach(([k, v]) => {
|
||||
if (v !== undefined && v !== null && v !== '') out[k] = v;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/* ---------- 选项常量(和数据库里实际存的值保持一致)---------- */
|
||||
const GENDER_OPTIONS = ['男', '女'];
|
||||
const ROLE_OPTIONS = ['班主任', '任课'];
|
||||
const EDU_OPTIONS = ['大专', '本科', '硕士', '博士', '其他'];
|
||||
|
||||
// 统计模块的年龄比较条件 -> 中文标签
|
||||
const AGE_OP_OPTIONS = [
|
||||
{ value: 'gt', label: '大于' },
|
||||
{ value: 'ge', label: '大于等于' },
|
||||
{ value: 'lt', label: '小于' },
|
||||
{ value: 'le', label: '小于等于' },
|
||||
{ value: 'eq', label: '等于' },
|
||||
{ value: 'between', label: '区间' },
|
||||
];
|
||||
|
||||
/* ---------- 统一提示 ---------- */
|
||||
function toastOk(msg) { ElementPlus.ElMessage.success(msg); }
|
||||
function toastErr(e) { ElementPlus.ElMessage.error(typeof e === 'string' ? e : (e && e.message) || '操作失败'); }
|
||||
|
||||
/* ---------- 跨模块下拉数据缓存 ----------
|
||||
学生表单要班级和顾问,老师表单要班级,就业和成绩要学生,
|
||||
这些列表在多个页面反复用,缓存一份,避免每次开弹窗都请求一遍。
|
||||
注意:/classes/ 的 limit 要显式给大值,默认 100 会把列表截断。 */
|
||||
|
||||
const Cache = {
|
||||
classes: [], advisors: [], students: [], loaded: {},
|
||||
|
||||
async loadClasses(force) {
|
||||
if (this.loaded.classes && !force) return this.classes;
|
||||
this.classes = await ClassApi.list({ limit: 1000 });
|
||||
this.loaded.classes = true;
|
||||
return this.classes;
|
||||
},
|
||||
async loadAdvisors(force) {
|
||||
if (this.loaded.advisors && !force) return this.advisors;
|
||||
this.advisors = await AdvisorApi.list({ limit: 1000 });
|
||||
this.loaded.advisors = true;
|
||||
return this.advisors;
|
||||
},
|
||||
async loadStudents(force) {
|
||||
if (this.loaded.students && !force) return this.students;
|
||||
this.students = await StudentApi.list({});
|
||||
this.loaded.students = true;
|
||||
return this.students;
|
||||
},
|
||||
// 数据被增删改之后,让缓存失效,下次用会重新拉
|
||||
invalidate(what) {
|
||||
if (what) delete this.loaded[what];
|
||||
else this.loaded = {};
|
||||
},
|
||||
};
|
||||
|
||||
// 班级表没有班级名列,只有编号和开课时间,所以统一显示成 "编号1 · 2026-09-01 开班"
|
||||
function className(id) {
|
||||
if (id === null || id === undefined) return '—';
|
||||
const c = Cache.classes.find(x => x.class_id === id);
|
||||
if (!c) return `编号${id}`;
|
||||
return `编号${id} · ${fmtDate(c.start_time)} 开班`;
|
||||
}
|
||||
|
||||
// 下拉框里更短的写法,表格里再用长的
|
||||
function classLabel(id) {
|
||||
if (id === null || id === undefined) return '未分班';
|
||||
const c = Cache.classes.find(x => x.class_id === id);
|
||||
return c ? `编号${id} · ${fmtDate(c.start_time)}` : `编号${id}`;
|
||||
}
|
||||
|
||||
function advisorName(id) {
|
||||
if (id === null || id === undefined) return '—';
|
||||
const a = Cache.advisors.find(x => x.adv_id === id);
|
||||
return a ? a.adv_name : `编号${id}`;
|
||||
}
|
||||
|
||||
// 学生下拉框的显示文案:学号 + 姓名
|
||||
function studentLabel(s) {
|
||||
return `${s.stu_no} ${dash(s.stu_name)}`;
|
||||
}
|
||||
|
||||
/* ---------- 侧边栏图标(内联 SVG,避免再多引一个图标库)---------- */
|
||||
const ICONS = {
|
||||
dashboard: '<path d="M3 3h7v7H3zM14 3h7v4h-7zM14 10h7v11h-7zM3 13h7v8H3z"/>',
|
||||
student: '<path d="M12 3 2 8l10 5 10-5-10-5z"/><path d="M6 10.5V16c0 1.7 2.7 3 6 3s6-1.3 6-3v-5.5"/>',
|
||||
class: '<path d="M3 6h18v13H3z"/><path d="M3 10h18M8 6V4h8v2"/>',
|
||||
teacher: '<circle cx="12" cy="8" r="3.5"/><path d="M4.5 20c0-3.6 3.4-6 7.5-6s7.5 2.4 7.5 6"/>',
|
||||
advisor: '<circle cx="9" cy="8" r="3"/><path d="M2.5 19c0-3.2 2.9-5.5 6.5-5.5s6.5 2.3 6.5 5.5"/><path d="M17 8h5M19.5 5.5v5"/>',
|
||||
emp: '<path d="M3 7h18v13H3z"/><path d="M9 7V4.5A1.5 1.5 0 0 1 10.5 3h3A1.5 1.5 0 0 1 15 4.5V7M3 12h18"/>',
|
||||
score: '<path d="M4 19V5M4 19h16"/><path d="M8 15l3.5-4 3 2.5L20 7"/>',
|
||||
};
|
||||
|
||||
function icon(name) {
|
||||
return `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor"
|
||||
stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">${ICONS[name] || ''}</svg>`;
|
||||
}
|
||||
Reference in New Issue
Block a user