Files
ClassHelper/packages/shared/src/constants.ts
T
LoveChengke 11008928a1 fix(island,homework,schedule,web): 16 项反馈修复(联合通知点不开 / 设置不生效 / 红点位置 / 看板当天且铺满 / 单双周与周数可调 / Web 手机端与批量通知等)
一、灵动岛
- ①「紧急通知后来了新作业,收起再点开不了新作业」根因有二,都已修:
  (a) active 仍是紧急那条,点击只会反复打开它 —— 现在点击胶囊优先打开**最新**那条未处理消息(队列里更晚的);
  (b) 上课时段 syncWindow 会把普通通知立刻隐藏(表现为点了没反应)—— 新增 explicitShowId,
      "用户主动点开"的那条守卫放行(岛完全隐藏时仍严格不打扰)。
  冒烟新增「紧急通知后来了新作业:收起再点击打开的是新作业(联合通知回归)」。
- ② 设置实时预览/保存:补「设置页真实链路」断言(window.desktop.islandSetAppearance → IPC → 主进程 →
  渲染进程 CSS 变量 + 窗口尺寸),并断言"岛显示中改尺寸立即生效"(实测均通过)。
- ③ 未读红点移到「通知」图标右上角(此前在文字后面),并加 DOM 几何断言(dx>0 且 dy<0,
  没有未读时自动造一条未读通知再断言、断言后删除)。
- 冒烟稳定性:新增 island.setBlurCollapseEnabled()(默认开),测试期间关掉"失焦收起",
  避免系统抢焦点导致展开态被收回的随机失败;专门验证失焦的用例仍临时打开。

二、作业看板(学生端)
- ⑥ 只看当天:新增「只看今天」开关(默认开),按 createdAt 的本地日期过滤,昨天的过期作业不再混入。
- ⑧ 完整显示:卡片同时渲染标题 + 作业要求全文(列表模式也加「作业要求」列)。
- ⑨ 铺满不滚动:看板外层高度固定 + overflow hidden,按内容/可用高度算缩放系数用 transform: scale 适配
  (工具栏显示当前缩放比例),页面本身不出现滚动条。
- ⑦ 全屏看板标题栏用 #header 插槽:标题左对齐、当天日期时间绝对居中(受「显示时间」开关控制,每分钟刷新)。

三、课表与周数
- ⑤ 学期周数可调:新增 Class.termWeeks(默认 20,迁移 + 维护脚本),班主任在班级管理里改;
  GET /api/dashboard/term?classId= 返回该班 maxWeek,课表周次下拉与新建课表的默认 weekEnd 都按它来
  (不再固定 30 周)。
- ⑩ 课表管理只保留「导入 ClassIsland 课程表」入口(时间表由课程表 JSON 的 TimeLayouts 一并带入),
  「导入时间配置」按钮下线;ui-smoke 第 7.7 段改为断言新弹窗(解析 4 行 + 单双周标签 + 旧入口已下线)。

四、以班级为单位(⑬)与 Web 端
- 个人学生账号登录停用:POST /api/auth/login 对 role=STUDENT 返回 403(改用班级码 + 班级密码);
  学生行仍作为个人数据记录键由班级会话代全班读写;e2e 全面切换到班级会话并新增该断言。
- ④ 作业科目过少:新增共享 SUBJECT_CATALOG(16 个常见科目)+ 种子课程扩到全科目;
  Web 作业发布弹窗可选「其他科目(新建)」,提交前自动建课(同名复用)。
- ⑪ 通知支持多班批量发布(多选 + 逐班发布 + 汇总结果 + 二次确认覆盖所有选中班);
  ⑫ 单班只显示已读情况、跨班才显示已读率;⑭ 手机端课表可横向滚动看全 7 天;
  ⑮ 手机端已连接不再显示孤立绿点(仅未连接时显示文字标签);⑯ 手机端「叫人/发布通知」按钮同宽左对齐;
  ⑬ Web 学生管理去掉「用户名」列(新建自动生成学号式用户名,导入仍支持显式指定)。

验证:verify:e2e 149/149、verify:web 20/20、verify:desktop 75/75(复跑 68/68,打包便携版 75/75 packaged=true)、
typecheck / lint / format 全绿;服务端与 Web 已重新打包并部署到本机安装目录。
2026-09-13 08:39:11 +08:00

267 lines
8.4 KiB
TypeScript
Raw 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.
import type { NotificationPriority, UserRole } from './types.js';
/** API 前缀 */
export const API_PREFIX = '/api';
/** REST 路径集中管理,避免前端散落硬编码字符串 */
export const API_PATHS = {
auth: {
login: '/auth/login',
logout: '/auth/logout',
me: '/auth/me',
changePassword: '/auth/password',
},
classes: '/classes',
courses: '/courses',
schedules: '/schedules',
homeworks: '/homeworks',
notifications: '/notifications',
/** 叫人:老师点名让学生过来 */
calls: '/calls',
grades: '/grades',
students: '/students',
teachers: '/teachers',
/** 导入:模板下载 / 表格导入 / ClassIsland 课表时间配置 */
imports: '/imports',
dashboard: '/dashboard/summary',
} as const;
/** 用户角色 */
export const USER_ROLES: readonly UserRole[] = ['ADMIN', 'TEACHER', 'STUDENT'];
export const ROLE_LABELS: Record<UserRole, string> = {
ADMIN: '系统管理员',
TEACHER: '教师',
STUDENT: '学生',
};
/** 通知优先级 */
export const NOTIFICATION_PRIORITIES: readonly NotificationPriority[] = ['LOW', 'NORMAL', 'HIGH', 'URGENT'];
export const PRIORITY_LABELS: Record<NotificationPriority, string> = {
LOW: '低',
NORMAL: '普通',
HIGH: '重要',
URGENT: '紧急',
};
/** Element Plus tag 类型,三端统一文案与配色 */
export const PRIORITY_TAG_TYPES: Record<NotificationPriority, 'info' | 'primary' | 'warning' | 'danger'> = {
LOW: 'info',
NORMAL: 'primary',
HIGH: 'warning',
URGENT: 'danger',
};
/** 星期:1=周一 ... 7=周日(与数据库 dayOfWeek 一致) */
export const WEEKDAY_LABELS: Record<number, string> = {
1: '周一',
2: '周二',
3: '周三',
4: '周四',
5: '周五',
6: '周六',
7: '周日',
};
export const WEEKDAYS: readonly number[] = [1, 2, 3, 4, 5, 6, 7];
/**
* 常用科目目录:作业/课表里"新建课程"时可直接选(也用于种子数据与导入时的科目建议)。
* 顺序按中学常见排课习惯,前 9 个是文化课,后面是技术与体艺类。
*/
export const SUBJECT_CATALOG: readonly string[] = [
'语文',
'数学',
'英语',
'物理',
'化学',
'生物',
'政治',
'历史',
'地理',
'信息技术',
'通用技术',
'体育',
'音乐',
'美术',
'心理健康',
'劳动',
] as const;
/**
* 单双周标签与取值(课表用)。
* 语义与 ClassIsland 课表一致:WeekCountDivTotal=1 → 每周;=2 时 WeekCountDiv=1 → 单周、=2 → 双周。
*/
export const WEEK_PARITY_LABELS: Record<'ALL' | 'ODD' | 'EVEN', string> = {
ALL: '每周',
ODD: '单周',
EVEN: '双周',
};
export const WEEK_PARITY_VALUES: readonly ('ALL' | 'ODD' | 'EVEN')[] = ['ALL', 'ODD', 'EVEN'];
/** 一个学期按多少周为一个单双周循环(ClassIsland WeekCountDivTotal 的默认值) */
export const WEEK_PARITY_CYCLE = 2;
/** 灵动岛默认外观(设置页个性化项的初始值,也是区间校验的单一来源) */
export const DEFAULT_ISLAND_APPEARANCE = {
height: 44,
width: 268,
radius: 20,
opacity: 1,
/** 默认强调色采用 Apple 系统蓝(与 WinIsland 的系统色体系一致) */
accent: '#0a84ff',
fontSize: 13,
animations: true,
speed: 1,
position: 'top-center',
alwaysOnTop: true,
/** 纯黑底(WinIsland default 风格) */
style: 'black',
/** 空闲时完全隐藏(与原行为一致;打开后空闲会留一条细缝) */
idleSliver: false,
} as const;
/** 个性化设置项的合法区间(前端滑块与后端校验共用,避免越界导致布局错乱) */
export const ISLAND_APPEARANCE_RANGES = {
height: { min: 36, max: 72 },
width: { min: 220, max: 420 },
radius: { min: 8, max: 32 },
opacity: { min: 0.4, max: 1 },
fontSize: { min: 11, max: 20 },
speed: { min: 0.5, max: 2 },
} as const;
/** 停靠位置可选值(与 WinIsland 的 DockPosition 一致:顶/底 × 左/中/右) */
export const ISLAND_POSITIONS = [
'top-center',
'top-left',
'top-right',
'bottom-center',
'bottom-left',
'bottom-right',
] as const;
export const ISLAND_POSITION_LABELS: Record<(typeof ISLAND_POSITIONS)[number], string> = {
'top-center': '顶部居中',
'top-left': '左上角',
'top-right': '右上角',
'bottom-center': '底部居中',
'bottom-left': '左下角',
'bottom-right': '右下角',
};
/** 视觉风格可选值(参考 WinIsland 的 island_style) */
export const ISLAND_STYLES = ['black', 'glass', 'tinted'] as const;
export const ISLAND_STYLE_LABELS: Record<(typeof ISLAND_STYLES)[number], string> = {
black: '纯黑(灵动岛默认)',
glass: '毛玻璃(半透明模糊)',
tinted: '主题色渐变',
};
/**
* 空闲细缝尺寸(参考 WinIsland hidden_width = 5:空闲态是一条很窄的圆角柱)。
* 宽度固定 6px,高度取胶囊高度的一部分,视觉上就是一条"小黑条"。
*/
export const ISLAND_SLIVER_WIDTH = 6;
/**
* 各形态相对"胶囊"的尺寸增量(**单一事实来源**,主进程与渲染进程共用):
* - 主进程据此计算"固定包围盒窗口"有多大(窗口只为最大形态留位置,开合时不改窗口);
* - 渲染进程据此计算岛的目标尺寸并做弹簧形变。
* 取值参考 WinIsland:胶囊 120×27 → 展开 360×200(宽 ×3、高 ×7.4),我们按内容需要取略小的比例。
*/
export const ISLAND_SIZE_DELTA = {
expanded: { width: 156, height: 186 },
urgent: { width: 172, height: 202 },
call: { width: 188, height: 218 },
} as const;
/** 阴影/光晕留白(窗口比最大形态多出的透明边,保证投影不被窗口裁掉) */
export const ISLAND_SHADOW_PAD = 10;
/** 岛内文本的排版系数(相对基础字号,集中在这里方便统一调整) */
export const ISLAND_TYPE_SCALE = {
/** 展开卡标题 */
title: 1.08,
/** 正文 */
body: 0.95,
/** 次要说明 / 元信息 */
meta: 0.78,
/** 徽标 / 按钮 */
badge: 0.76,
/** 胶囊主标题 */
pillTitle: 0.92,
/** 胶囊副标题 */
pillSub: 0.74,
} as const;
/** 默认学期周次上限 */
export const DEFAULT_WEEK_COUNT = 20;
/** WebSocket 事件名(服务端广播 / 客户端监听保持一致) */
export const SOCKET_EVENTS = {
notificationNew: 'notification:new',
homeworkNew: 'homework:new',
homeworkUpdated: 'homework:updated',
homeworkStatus: 'homework:status',
gradeUpdated: 'grade:updated',
scheduleUpdated: 'schedule:updated',
classUpdated: 'class:updated',
/** "叫人":老师点名让某位同学去找他(定向到 user:{studentId} 房间) */
callNew: 'call:new',
connected: 'connected',
} as const;
/**
* "叫人"快捷短语(Web 管理端一键选择,学生端灵动岛同步展示)。
* 自定义消息会替换/补全这些短语;`message` 为空时用短语本身。
*/
export const CALL_QUICK_PHRASES: readonly string[] = [
'请到办公室找我',
'请到讲台找我',
'请带上作业本找我',
'请带上试卷找我',
'请到实验室找我',
'请到门卫处找我',
'请到教室门口等我',
'请马上来一趟',
] as const;
/** 叫人消息标题模板:请 XXX 同学找 XXX 老师(老师名字已含"老师"时不重复追加) */
export function buildCallTitle(studentName: string, teacherName: string): string {
const teacher = /老师|教师|主任|校长/.test(teacherName) ? teacherName : `${teacherName} 老师`;
return `请 ${studentName} 同学找 ${teacher}`;
}
/** Socket.IO 房间名生成规则:与后端保持一致,客户端订阅时复用 */
export const SOCKET_ROOMS = {
class: (classId: string): string => `class:${classId}`,
user: (userId: string): string => `user:${userId}`,
teacher: (teacherId: string): string => `teacher:${teacherId}`,
role: (role: UserRole): string => `role:${role}`,
students: 'students',
teachers: 'teachers',
} as const;
/** 本地持久化键(Web 端 localStorage / EXE 端 IndexedDB 命名空间) */
export const STORAGE_KEYS = {
token: 'classhelper.token',
user: 'classhelper.user',
serverUrl: 'classhelper.serverUrl',
cachePrefix: 'classhelper.cache',
lastSyncAt: 'classhelper.lastSyncAt',
} as const;
/** 桌面客户端默认后端地址 */
export const DEFAULT_SERVER_URL = 'http://127.0.0.1:4000';
/** 默认端口与分页 */
export const DEFAULT_SERVER_PORT = 4000;
export const DEFAULT_PAGE_SIZE = 20;
export const MAX_PAGE_SIZE = 200;
/** 教师与管理员角色集合,便于 RBAC 判断 */
export const STAFF_ROLES: readonly UserRole[] = ['ADMIN', 'TEACHER'];