feat(homeworks,island,classisland,desktop,web,server): 作业按天查看与教室机器录入 + 提醒上课时段暂存规则 + 灵动岛边距/跟随鼠标屏幕
- 作业:新增 Homework.assignDate(本地日期 YYYY-MM-DD;UTC 切片在晚上会把今天的作业算成明天),列表按天过滤与排序,新增 GET /homeworks?date= 与 GET /homeworks/days(日期高亮)供客户端与 Web 共用;客户端作业页改为"看作业 + 录作业"(不再显示完成状态),支持快捷短语(存本机配置,「设置 → 作业录入」可增删/恢复默认) - 教室机器录入:POST /homeworks 去掉 requireRole,权限下沉到 homeworks.service(staff 或本班班级账号);班级账号的 sub 是班级 id,写 createdBy 前换成该班班主任,避免撞 User 外键 - 提醒类型:ClassIslandPush.kind(notification / call)。通知类上课时段暂存、下课补弹,叫人/紧急立即弹;客户端灵动岛与 ClassIsland 插件共用同一套规则 - 灵动岛:新增 marginX / marginY(默认仍 8px,不动老外观)与「跟随鼠标屏幕」;文案统一成"灵动岛 / ClassIsland" - 设置页:新增「ClassIsland 联动」(联动状态 + 通知模式)与「作业录入」分区 - 校验:verify:e2e 增补 assignDate 默认当天 / ?date= 过滤 / days / 非法日期 422 / 班级账号录入 201 与跨班 403;verify:classisland 与插件静态契约增补上课时段暂存断言(57/57) - 文档:README 与 AGENTS.md 同步(作业按天、上课时段策略、两个方向的开关表、新增踩坑) 门禁:pnpm lint 0 error、pnpm typecheck 四包通过、verify:classisland-plugin 57/57 通过。 未跑:verify:e2e / verify:classisland(需后端启动)、verify:web / verify:desktop(需先构建产物)。
This commit is contained in:
1 parent
d0830f6830
commit
7536abfc95
33 files changed
+1236
-162
No files matched your search
@@ -126,6 +126,15 @@ export const DEFAULT_ISLAND_APPEARANCE = {
|
||||
style: 'black',
|
||||
/** 空闲时完全隐藏(与原行为一致;打开后空闲会留一条细缝) */
|
||||
idleSliver: false,
|
||||
/**
|
||||
* 距屏幕左右边缘的距离(px):停靠左/右时生效。
|
||||
* 默认与原行为一致(8px)—— 加这个设置是为了能调,不是为了改默认外观。
|
||||
*/
|
||||
marginX: 8,
|
||||
/** 距屏幕上下边缘的距离(px):停靠顶/底时生效 */
|
||||
marginY: 8,
|
||||
/** 是否跟随鼠标所在屏幕(多显示器教室电脑:鼠标在哪块屏就在哪块屏显示) */
|
||||
followCursorDisplay: false,
|
||||
} as const;
|
||||
|
||||
/** 个性化设置项的合法区间(前端滑块与后端校验共用,避免越界导致布局错乱) */
|
||||
@@ -136,6 +145,8 @@ export const ISLAND_APPEARANCE_RANGES = {
|
||||
opacity: { min: 0.4, max: 1 },
|
||||
fontSize: { min: 11, max: 20 },
|
||||
speed: { min: 0.5, max: 2 },
|
||||
marginX: { min: 0, max: 200 },
|
||||
marginY: { min: 0, max: 160 },
|
||||
} as const;
|
||||
|
||||
/** 停靠位置可选值(与 WinIsland 的 DockPosition 一致:顶/底 × 左/中/右) */
|
||||
@@ -264,19 +275,44 @@ export const CLASSISLAND_TIME_STATE_LABELS: Record<string, string> = {
|
||||
export const CLASSISLAND_NOTIFICATION_CHANNELS = ['both', 'client', 'classisland'] as const;
|
||||
|
||||
export const CLASSISLAND_NOTIFICATION_CHANNEL_LABELS: Record<string, string> = {
|
||||
both: 'ClassHelper 与 ClassIsland 都弹',
|
||||
client: '只在 ClassHelper 客户端弹',
|
||||
classisland: '只在 ClassIsland 上弹',
|
||||
both: '两者都弹',
|
||||
client: '灵动岛(ClassHelper 客户端)',
|
||||
classisland: 'ClassIsland',
|
||||
};
|
||||
|
||||
export const CLASSISLAND_NOTIFICATION_CHANNEL_HINTS: Record<string, string> = {
|
||||
both: '教室的 ClassHelper 客户端与 ClassIsland 都会提醒(默认)',
|
||||
client: '提醒只在 ClassHelper 客户端显示,不推送到 ClassIsland',
|
||||
classisland: '提醒只推送到 ClassIsland,ClassHelper 客户端只留记录、不弹窗',
|
||||
both: '灵动岛与 ClassIsland 都会提醒老师发的内容(默认)',
|
||||
client: '只在 ClassHelper 客户端的灵动岛提醒,不推送到 ClassIsland',
|
||||
classisland: '只推送到 ClassIsland 全屏提醒,灵动岛不弹(通知中心仍留记录)',
|
||||
};
|
||||
|
||||
/** 默认显示位置:两端都弹 */
|
||||
export const DEFAULT_CLASSISLAND_NOTIFICATION_CHANNEL = 'both';
|
||||
/**
|
||||
* 作业录入的默认快捷短语(客户端「设置 → 作业录入」可增删)。
|
||||
*
|
||||
* 教室里老师录作业的常见口径:科目简称(P=拼音)、本子类型(大本/小本)、
|
||||
* 作业动作(背诵/默写/听写/预习/订正)—— 点一下就追加到内容里,少打字。
|
||||
*/
|
||||
export const HOMEWORK_PHRASE_DEFAULTS = [
|
||||
'P',
|
||||
'大本',
|
||||
'小本',
|
||||
'卷子',
|
||||
'背诵',
|
||||
'默写',
|
||||
'听写',
|
||||
'预习',
|
||||
'订正',
|
||||
'读书',
|
||||
] as const;
|
||||
|
||||
/** 单条快捷短语的最大长度(防止把整篇作业粘进去) */
|
||||
export const HOMEWORK_PHRASE_MAX_LENGTH = 20;
|
||||
|
||||
/** 快捷短语最多几条 */
|
||||
export const HOMEWORK_PHRASE_MAX_COUNT = 24;
|
||||
|
||||
/** 默认提醒显示时长(秒),后端与插件共用同一口径 */
|
||||
export const CLASSISLAND_NOTIFICATION_DEFAULT_DURATION = 8;
|
||||
export const CLASSISLAND_NOTIFICATION_MAX_DURATION = 120;
|
||||
|
||||
@@ -279,6 +279,13 @@ export interface HomeworkDto {
|
||||
attachmentUrl: string | null;
|
||||
createdBy: string;
|
||||
createdAt: string;
|
||||
/**
|
||||
* 作业所属日期(YYYY-MM-DD,**不带时区**)。
|
||||
*
|
||||
* 与 createdAt 的区别:createdAt 是"什么时候录的",assignDate 是"这天的作业"。
|
||||
* 客户端 / Web 的按天查看与日期高亮都用它,避免跨时区把作业算到前一天。
|
||||
*/
|
||||
assignDate: string;
|
||||
course?: CourseBrief | null;
|
||||
creator?: ClassTeacherBrief | null;
|
||||
/** 当前登录学生的完成状态(学生端接口返回) */
|
||||
@@ -288,6 +295,19 @@ export interface HomeworkDto {
|
||||
completedCount?: number;
|
||||
}
|
||||
|
||||
/** 某一天有作业(用于日期选择器高亮) */
|
||||
export interface HomeworkDaySummary {
|
||||
/** YYYY-MM-DD */
|
||||
date: string;
|
||||
/** 当天作业条数 */
|
||||
count: number;
|
||||
}
|
||||
|
||||
/** GET /api/homeworks/days 的返回 */
|
||||
export interface HomeworkDaysDto {
|
||||
days: HomeworkDaySummary[];
|
||||
}
|
||||
|
||||
export interface HomeworkStatusDto {
|
||||
id: string;
|
||||
homeworkId: string;
|
||||
@@ -301,6 +321,8 @@ export interface CreateHomeworkRequest {
|
||||
courseId?: string | null;
|
||||
title: string;
|
||||
content: string;
|
||||
/** 作业所属日期(YYYY-MM-DD);不传按服务器当天处理 */
|
||||
assignDate?: string | null;
|
||||
attachmentUrl?: string | null;
|
||||
}
|
||||
|
||||
@@ -541,6 +563,12 @@ export interface IslandAppearance {
|
||||
* 关闭时(默认)空闲即完全隐藏,与原行为一致。
|
||||
*/
|
||||
idleSliver: boolean;
|
||||
/** 距屏幕左右边缘的距离(px):停靠左/右时生效 */
|
||||
marginX: number;
|
||||
/** 距屏幕上下边缘的距离(px):停靠顶/底时生效 */
|
||||
marginY: number;
|
||||
/** 是否跟随鼠标所在屏幕(多显示器教室电脑用) */
|
||||
followCursorDisplay: boolean;
|
||||
}
|
||||
|
||||
/** 灵动岛停靠位置(6 个锚点,与 WinIsland 一致) */
|
||||
@@ -882,6 +910,12 @@ export interface ClassIslandPushNotification {
|
||||
createdAt: string;
|
||||
/** 是否紧急(紧急时插件用更强的遮罩与更长的时长) */
|
||||
urgent: boolean;
|
||||
/**
|
||||
* 提醒类型:
|
||||
* - `notification`:通知类提醒 —— **上课时段插件会暂存,下课后才弹**;
|
||||
* - `call`:叫人等「主动通知」—— 不论是否上课都立刻弹(老师正在等学生)。
|
||||
*/
|
||||
kind: 'notification' | 'call';
|
||||
classId: string;
|
||||
className?: string | null;
|
||||
teacherName?: string | null;
|
||||
|
||||
@@ -11,6 +11,35 @@ function pad(input: number): string {
|
||||
return input < 10 ? `0${input}` : String(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地日期串(YYYY-MM-DD)。
|
||||
*
|
||||
* 「作业属于哪一天」用它是**本地**日期,不是 UTC 日期:教室在 UTC+8,晚上 8 点录的作业
|
||||
* 应该算今天,而 `toISOString().slice(0, 10)` 会把它算成明天(UTC 已跨天)。
|
||||
*/
|
||||
export function dayKeyLocal(value: string | number | Date): string {
|
||||
const date = toDate(value);
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||||
}
|
||||
|
||||
/** 判断字符串是否形如 YYYY-MM-DD(并校验是真实存在的日期) */
|
||||
export function isDayKey(value: string): boolean {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
|
||||
const [year, month, day] = value.split('-').map(Number);
|
||||
const date = new Date(year, month - 1, day);
|
||||
return (
|
||||
date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day
|
||||
);
|
||||
}
|
||||
|
||||
/** 在 YYYY-MM-DD 上加减天数(用于日期选择器的区间与"昨天/明天") */
|
||||
export function shiftDayKey(value: string, deltaDays: number): string {
|
||||
const [year, month, day] = value.split('-').map(Number);
|
||||
const date = new Date(year, month - 1, day + deltaDays);
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||||
}
|
||||
|
||||
/** 格式化为 YYYY-MM-DD,withTime=true 时追加 HH:mm */
|
||||
export function formatDate(value: string | number | Date | null | undefined, withTime = false): string {
|
||||
if (value === null || value === undefined || value === '') return '';
|
||||
|
||||
Reference in new issue
Block a user