feat(shared,prisma): 建立日课表与岛组件契约及数据范围模型
This commit is contained in:
1 parent
c0ebf88138
commit
66ef878a71
11 files changed
+758
-11
No files matched your search
@@ -0,0 +1,88 @@
|
||||
import type { ClassPeriod as ContractClassPeriod } from './types.js';
|
||||
import type { ScheduleDto } from './types.js';
|
||||
|
||||
export const TIME_POINT_TYPES = ['LESSON', 'BREAK', 'DIVIDER', 'ACTION'] as const;
|
||||
export type TimePointType = (typeof TIME_POINT_TYPES)[number];
|
||||
export interface TimePointDto {
|
||||
id: string;
|
||||
position: number;
|
||||
name: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
timeType: TimePointType;
|
||||
isHideDefault: boolean;
|
||||
defaultClassId: string | null;
|
||||
breakName: string;
|
||||
actionSet: string;
|
||||
}
|
||||
export interface TimeLayoutDetailDto {
|
||||
id: string;
|
||||
name: string;
|
||||
classId: string | null;
|
||||
points: TimePointDto[];
|
||||
}
|
||||
export interface DailyPlanEntryDto {
|
||||
id: string;
|
||||
timeLayoutItemId: string;
|
||||
courseId: string | null;
|
||||
isEnabled: boolean;
|
||||
isChangedClass: boolean;
|
||||
location: string | null;
|
||||
}
|
||||
export interface ClassPlanDto {
|
||||
id: string;
|
||||
classId: string;
|
||||
name: string;
|
||||
timeLayoutId: string;
|
||||
associatedGroup: string | null;
|
||||
weekday: number;
|
||||
weekCountDiv: number;
|
||||
weekCountDivTotal: number;
|
||||
weekStart: number;
|
||||
weekEnd: number;
|
||||
isEnabled: boolean;
|
||||
isOverlay: boolean;
|
||||
overlaySourceId: string | null;
|
||||
entries: DailyPlanEntryDto[];
|
||||
}
|
||||
export interface ActiveClassPlanDto {
|
||||
classPlan: ClassPlanDto | null;
|
||||
timeLayout: TimeLayoutDetailDto | null;
|
||||
schedules: ScheduleDto[];
|
||||
current: ContractClassPeriod | null;
|
||||
next: ContractClassPeriod | null;
|
||||
inClass: boolean;
|
||||
week: number;
|
||||
}
|
||||
export interface ActiveClassPlanWeekDto {
|
||||
classId: string;
|
||||
week: number;
|
||||
days: Array<ActiveClassPlanDto & { weekday: number; date: string }>;
|
||||
}
|
||||
|
||||
/** 设置来自服务端;离线/未初始化时统一回落 30。 */
|
||||
export function getMaxTermWeeks(configured?: unknown): number {
|
||||
const value = Number(configured);
|
||||
return Number.isInteger(value) && value >= 1 && value <= 60 ? value : 30;
|
||||
}
|
||||
|
||||
export type PermissionScope = 'SELF_CLASS' | 'GRADE' | 'DIVISION' | 'ALL';
|
||||
export interface ReleaseProgressDto {
|
||||
version: string;
|
||||
stage: 'idle' | 'downloading' | 'verifying' | 'ready' | 'waiting' | 'installing' | 'failed';
|
||||
downloadedBytes: number;
|
||||
totalBytes: number;
|
||||
percent: number | null;
|
||||
error: string;
|
||||
}
|
||||
export interface CollectiveGradeAnalysisDto {
|
||||
mode: 'collective';
|
||||
classId: string;
|
||||
suppressed: boolean;
|
||||
averagePercent: number | null;
|
||||
passRate: number | null;
|
||||
excellentRate: number | null;
|
||||
subjects: { name: string; averagePercent: number | null }[];
|
||||
distribution: { label: string; count: number | null }[];
|
||||
trend: { examName: string; averagePercent: number | null }[];
|
||||
}
|
||||
@@ -487,6 +487,7 @@ export const SOCKET_EVENTS = {
|
||||
homeworkStatus: 'homework:status',
|
||||
gradeUpdated: 'grade:updated',
|
||||
scheduleUpdated: 'schedule:updated',
|
||||
clientReleaseProgress: 'client:release-progress',
|
||||
classUpdated: 'class:updated',
|
||||
/** "叫人":老师点名让某位同学去找他(定向到 user:{studentId} 房间) */
|
||||
callNew: 'call:new',
|
||||
|
||||
@@ -6,4 +6,6 @@ export * from './constants.js';
|
||||
export * from './motion.js';
|
||||
export * from './permissions.js';
|
||||
export * from './utils.js';
|
||||
export * from './class-plan.js';
|
||||
export * from './island-layout.js';
|
||||
export type * from './types.js';
|
||||
@@ -0,0 +1,279 @@
|
||||
export const ISLAND_COMPONENTS = [
|
||||
'date',
|
||||
'clock',
|
||||
'countdown',
|
||||
'schedule',
|
||||
'text',
|
||||
'separator',
|
||||
'slide',
|
||||
'rolling',
|
||||
'group',
|
||||
'stack',
|
||||
'notification',
|
||||
'homework',
|
||||
'call',
|
||||
] as const;
|
||||
export type IslandComponentKind = (typeof ISLAND_COMPONENTS)[number];
|
||||
export const ISLAND_COMPONENT_LABELS: Record<IslandComponentKind, string> = {
|
||||
date: '日期',
|
||||
clock: '时钟',
|
||||
countdown: '倒计时',
|
||||
schedule: '课程表',
|
||||
text: '文本',
|
||||
separator: '分隔符',
|
||||
slide: '轮播',
|
||||
rolling: '滚动',
|
||||
group: '分组',
|
||||
stack: '堆叠',
|
||||
notification: '通知',
|
||||
homework: '作业',
|
||||
call: '叫人',
|
||||
};
|
||||
export interface ComponentAppearance {
|
||||
resourceOverridingEnabled: boolean;
|
||||
secondaryFontSize: number;
|
||||
bodyFontSize: number;
|
||||
emphasizedFontSize: number;
|
||||
largeFontSize: number;
|
||||
customForegroundEnabled: boolean;
|
||||
foreground: string;
|
||||
customBackgroundEnabled: boolean;
|
||||
background: string;
|
||||
customBackgroundOpacityEnabled: boolean;
|
||||
backgroundOpacity: number;
|
||||
customCornerRadiusEnabled: boolean;
|
||||
cornerRadius: number;
|
||||
opacity: number;
|
||||
}
|
||||
export interface IslandComponentNode {
|
||||
id: string;
|
||||
kind: IslandComponentKind;
|
||||
settings: {
|
||||
text: string;
|
||||
template: boolean;
|
||||
hour12: boolean;
|
||||
seconds: boolean;
|
||||
target: 'next' | 'school';
|
||||
display: 'current' | 'next' | 'today';
|
||||
count: number;
|
||||
showPeriod: boolean;
|
||||
direction: 'horizontal' | 'vertical';
|
||||
gap: number;
|
||||
interval: number;
|
||||
speed: number;
|
||||
};
|
||||
appearance: ComponentAppearance;
|
||||
advanced: {
|
||||
hideRules: ('in-class' | 'no-message' | 'no-class')[];
|
||||
align: 'stretch' | 'start' | 'center' | 'end';
|
||||
width: number;
|
||||
maxWidth: number;
|
||||
};
|
||||
children: IslandComponentNode[];
|
||||
}
|
||||
export interface IslandLayoutRow {
|
||||
id: string;
|
||||
name: string;
|
||||
main: boolean;
|
||||
allowNotifications: boolean;
|
||||
gap: number;
|
||||
nodes: IslandComponentNode[];
|
||||
}
|
||||
export interface IslandLayoutScheme {
|
||||
id: string;
|
||||
name: string;
|
||||
rows: IslandLayoutRow[];
|
||||
scale: number;
|
||||
color: string;
|
||||
foreground: string;
|
||||
opacity: number;
|
||||
radius: number;
|
||||
separated: boolean;
|
||||
spacing: number;
|
||||
}
|
||||
export interface IslandLayoutSettings {
|
||||
layout: 'fixed' | 'custom';
|
||||
activeSchemeId: string;
|
||||
schemes: IslandLayoutScheme[];
|
||||
}
|
||||
export function islandNode(kind: IslandComponentKind, id: string): IslandComponentNode {
|
||||
return {
|
||||
id,
|
||||
kind,
|
||||
children: [],
|
||||
settings: {
|
||||
text: '班级信息',
|
||||
template: false,
|
||||
hour12: false,
|
||||
seconds: true,
|
||||
target: 'next',
|
||||
display: 'current',
|
||||
count: 5,
|
||||
showPeriod: true,
|
||||
direction: 'horizontal',
|
||||
gap: 8,
|
||||
interval: 5,
|
||||
speed: 30,
|
||||
},
|
||||
appearance: {
|
||||
resourceOverridingEnabled: false,
|
||||
secondaryFontSize: 12,
|
||||
bodyFontSize: 16,
|
||||
emphasizedFontSize: 20,
|
||||
largeFontSize: 28,
|
||||
customForegroundEnabled: false,
|
||||
foreground: '#ffffff',
|
||||
customBackgroundEnabled: false,
|
||||
background: '#202024',
|
||||
customBackgroundOpacityEnabled: false,
|
||||
backgroundOpacity: 0.5,
|
||||
customCornerRadiusEnabled: false,
|
||||
cornerRadius: 16,
|
||||
opacity: 1,
|
||||
},
|
||||
advanced: { hideRules: [], align: 'stretch', width: 0, maxWidth: 0 },
|
||||
};
|
||||
}
|
||||
export function defaultIslandLayouts(): IslandLayoutSettings {
|
||||
return {
|
||||
layout: 'fixed',
|
||||
activeSchemeId: 'default',
|
||||
schemes: [
|
||||
{
|
||||
id: 'default',
|
||||
name: '默认组件方案',
|
||||
rows: [
|
||||
{
|
||||
id: 'main',
|
||||
name: '主要行',
|
||||
main: true,
|
||||
allowNotifications: true,
|
||||
gap: 12,
|
||||
nodes: [
|
||||
islandNode('date', 'date'),
|
||||
islandNode('clock', 'clock'),
|
||||
islandNode('schedule', 'schedule'),
|
||||
islandNode('notification', 'messages'),
|
||||
],
|
||||
},
|
||||
],
|
||||
scale: 1,
|
||||
color: '#202024',
|
||||
foreground: '#ffffff',
|
||||
opacity: 0.85,
|
||||
radius: 24,
|
||||
separated: false,
|
||||
spacing: 8,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
/** 导入/IPC 配置有同一边界:最多 12 行/200 节点/6 层,颜色和数值全部归一化。 */
|
||||
export function normalizeIslandLayouts(input: unknown): IslandLayoutSettings {
|
||||
const fallback = defaultIslandLayouts();
|
||||
if (!input || typeof input !== 'object') return fallback;
|
||||
const raw = input as IslandLayoutSettings;
|
||||
const clamp = (n: unknown, min: number, max: number, value: number) =>
|
||||
typeof n === 'number' && Number.isFinite(n) ? Math.min(max, Math.max(min, n)) : value;
|
||||
const color = (v: unknown, value: string) =>
|
||||
typeof v === 'string' && /^#[\da-f]{6}$/i.test(v) ? v : value;
|
||||
let count = 0;
|
||||
const ids = new Set<string>();
|
||||
const node = (n: IslandComponentNode, depth: number): IslandComponentNode | null => {
|
||||
if (!n || depth > 6 || ++count > 200 || !ISLAND_COMPONENTS.includes(n.kind)) return null;
|
||||
const id = typeof n.id === 'string' && n.id && !ids.has(n.id) ? n.id.slice(0, 100) : `node-${count}`;
|
||||
ids.add(id);
|
||||
const base = islandNode(n.kind, id),
|
||||
a = n.appearance || base.appearance,
|
||||
s = n.settings || base.settings,
|
||||
adv = n.advanced || base.advanced;
|
||||
return {
|
||||
...base,
|
||||
settings: {
|
||||
...base.settings,
|
||||
text: String(s.text || '').slice(0, 2000),
|
||||
template: s.template === true,
|
||||
hour12: s.hour12 === true,
|
||||
seconds: s.seconds !== false,
|
||||
target: s.target === 'school' ? 'school' : 'next',
|
||||
display: ['current', 'next', 'today'].includes(s.display) ? s.display : 'current',
|
||||
count: clamp(s.count, 1, 40, 5),
|
||||
showPeriod: s.showPeriod !== false,
|
||||
direction: s.direction === 'vertical' ? 'vertical' : 'horizontal',
|
||||
gap: clamp(s.gap, 0, 64, 8),
|
||||
interval: clamp(s.interval, 1, 300, 5),
|
||||
speed: clamp(s.speed, 1, 200, 30),
|
||||
},
|
||||
appearance: {
|
||||
resourceOverridingEnabled: a.resourceOverridingEnabled === true,
|
||||
secondaryFontSize: clamp(a.secondaryFontSize, 8, 96, 12),
|
||||
bodyFontSize: clamp(a.bodyFontSize, 8, 96, 16),
|
||||
emphasizedFontSize: clamp(a.emphasizedFontSize, 8, 96, 20),
|
||||
largeFontSize: clamp(a.largeFontSize, 8, 128, 28),
|
||||
customForegroundEnabled: a.customForegroundEnabled === true,
|
||||
foreground: color(a.foreground, '#ffffff'),
|
||||
customBackgroundEnabled: a.customBackgroundEnabled === true,
|
||||
background: color(a.background, '#202024'),
|
||||
customBackgroundOpacityEnabled: a.customBackgroundOpacityEnabled === true,
|
||||
backgroundOpacity: clamp(a.backgroundOpacity, 0, 1, 0.5),
|
||||
customCornerRadiusEnabled: a.customCornerRadiusEnabled === true,
|
||||
cornerRadius: clamp(a.cornerRadius, 0, 100, 16),
|
||||
opacity: clamp(a.opacity, 0, 1, 1),
|
||||
},
|
||||
advanced: {
|
||||
hideRules: Array.isArray(adv.hideRules)
|
||||
? adv.hideRules.filter((r) => ['in-class', 'no-message', 'no-class'].includes(r))
|
||||
: [],
|
||||
align: ['stretch', 'start', 'center', 'end'].includes(adv.align) ? adv.align : 'stretch',
|
||||
width: clamp(adv.width, 0, 2000, 0),
|
||||
maxWidth: clamp(adv.maxWidth, 0, 2000, 0),
|
||||
},
|
||||
children: Array.isArray(n.children)
|
||||
? n.children.map((c) => node(c, depth + 1)).filter((n): n is IslandComponentNode => Boolean(n))
|
||||
: [],
|
||||
};
|
||||
};
|
||||
const schemes: IslandLayoutScheme[] = [];
|
||||
for (const scheme of (Array.isArray(raw.schemes) ? raw.schemes : []).slice(0, 20)) {
|
||||
if (!scheme || !Array.isArray(scheme.rows)) continue;
|
||||
const rows = scheme.rows
|
||||
.filter((r) => r && typeof r === 'object')
|
||||
.slice(0, 12)
|
||||
.map((r, i) => ({
|
||||
id: String(r.id || `row-${i}`).slice(0, 100),
|
||||
name: String(r.name || `行 ${i + 1}`).slice(0, 64),
|
||||
main: r.main === true,
|
||||
allowNotifications: r.allowNotifications !== false,
|
||||
gap: clamp(r.gap, 0, 64, 8),
|
||||
nodes: (Array.isArray(r.nodes) ? r.nodes : [])
|
||||
.map((n) => node(n, 0))
|
||||
.filter((n): n is IslandComponentNode => Boolean(n)),
|
||||
}));
|
||||
if (!rows.length) continue;
|
||||
if (!rows.some((r) => r.main)) rows[0]!.main = true;
|
||||
let main = false;
|
||||
for (const r of rows) {
|
||||
if (r.main && main) r.main = false;
|
||||
if (r.main) main = true;
|
||||
}
|
||||
schemes.push({
|
||||
id: String(scheme.id || `scheme-${schemes.length}`).slice(0, 100),
|
||||
name: String(scheme.name || '组件方案').slice(0, 64),
|
||||
rows,
|
||||
scale: clamp(scheme.scale, 0.5, 3, 1),
|
||||
color: color(scheme.color, '#202024'),
|
||||
foreground: color(scheme.foreground, '#ffffff'),
|
||||
opacity: clamp(scheme.opacity, 0.1, 1, 0.85),
|
||||
radius: clamp(scheme.radius, 0, 100, 24),
|
||||
separated: scheme.separated === true,
|
||||
spacing: clamp(scheme.spacing, 0, 100, 8),
|
||||
});
|
||||
}
|
||||
return {
|
||||
layout: raw.layout === 'custom' ? 'custom' : 'fixed',
|
||||
schemes: schemes.length ? schemes : fallback.schemes,
|
||||
activeSchemeId: schemes.some((s) => s.id === raw.activeSchemeId)
|
||||
? raw.activeSchemeId
|
||||
: schemes[0]?.id || 'default',
|
||||
};
|
||||
}
|
||||
@@ -172,6 +172,7 @@ export const PERMISSION_KEYS = [
|
||||
'teacher:manage',
|
||||
'schedule:read',
|
||||
'schedule:write',
|
||||
'timetable:manage',
|
||||
// 内容发布
|
||||
'homework:read',
|
||||
'homework:write',
|
||||
@@ -191,6 +192,7 @@ export const PERMISSION_KEYS = [
|
||||
// 设备与联动
|
||||
'client:read',
|
||||
'client:manage',
|
||||
'client:update',
|
||||
'client:update-push',
|
||||
'remote:control',
|
||||
// 学期与归档
|
||||
@@ -210,6 +212,41 @@ export const PERMISSION_KEYS = [
|
||||
|
||||
export type PermissionKey = (typeof PERMISSION_KEYS)[number];
|
||||
|
||||
/** 默认角色组之外的教师岗位组;空年级范围必须由管理员配置,不能自动授权全校。 */
|
||||
const TEACHING_KEYS: PermissionKey[] = [
|
||||
'homework:write',
|
||||
'grade:write',
|
||||
'notification:publish',
|
||||
'call:create',
|
||||
'schedule:read',
|
||||
'roster:read',
|
||||
'grade:read',
|
||||
'homework:read',
|
||||
'notification:read',
|
||||
];
|
||||
const LEAD_KEYS: PermissionKey[] = [
|
||||
'client:read',
|
||||
'client:manage',
|
||||
'remote:control',
|
||||
'roster:read',
|
||||
'class:read',
|
||||
'schedule:read',
|
||||
'grade:read',
|
||||
'dashboard:read',
|
||||
];
|
||||
export const BUILTIN_TEACHER_GROUPS = [
|
||||
{
|
||||
id: 'builtin-head',
|
||||
name: '班主任',
|
||||
scope: 'SELF_CLASS',
|
||||
scopeValue: 'HEAD',
|
||||
keys: [...TEACHING_KEYS, 'client:read', 'client:manage'],
|
||||
},
|
||||
{ id: 'builtin-teacher', name: '教师', scope: 'SELF_CLASS', scopeValue: '', keys: TEACHING_KEYS },
|
||||
{ id: 'builtin-grade', name: '级长', scope: 'GRADE', scopeValue: '', keys: LEAD_KEYS },
|
||||
{ id: 'builtin-division', name: '部长', scope: 'DIVISION', scopeValue: '', keys: LEAD_KEYS },
|
||||
] as const;
|
||||
|
||||
/** 权限键的中文名(权限组管理页与文档展示,别再各写一份) */
|
||||
export const PERMISSION_LABELS: Record<PermissionKey, string> = {
|
||||
'class:read': '查看班级与班级列表',
|
||||
@@ -220,6 +257,8 @@ export const PERMISSION_LABELS: Record<PermissionKey, string> = {
|
||||
'teacher:manage': '教师账号管理(新建 / 导入 / 重置密码 / 删除)',
|
||||
'schedule:read': '查看课表',
|
||||
'schedule:write': '课表与课程编辑',
|
||||
'timetable:manage': '管理全校时间表',
|
||||
'client:update': '管理客户端安装包与更新策略',
|
||||
'homework:read': '查看作业',
|
||||
'homework:write': '发布 / 修改 / 删除作业(仍按任教科目录入)',
|
||||
'notification:read': '查看通知',
|
||||
@@ -264,6 +303,7 @@ export const PERMISSION_CATEGORIES: readonly { title: string; keys: readonly Per
|
||||
'teacher:manage',
|
||||
'schedule:read',
|
||||
'schedule:write',
|
||||
'timetable:manage',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -292,7 +332,7 @@ export const PERMISSION_CATEGORIES: readonly { title: string; keys: readonly Per
|
||||
},
|
||||
{
|
||||
title: '客户端设备',
|
||||
keys: ['client:read', 'client:manage', 'client:update-push', 'remote:control'],
|
||||
keys: ['client:read', 'client:manage', 'client:update', 'client:update-push', 'remote:control'],
|
||||
},
|
||||
{ title: '学期与统计', keys: ['term:read', 'term:write', 'archive:manage', 'dashboard:read'] },
|
||||
{
|
||||
@@ -327,6 +367,8 @@ export const PERMISSION_DEFAULT_ROLES: Record<PermissionKey, readonly UserRole[]
|
||||
'teacher:manage': ['ADMIN'],
|
||||
'schedule:read': ['ADMIN', 'TEACHER'],
|
||||
'schedule:write': ['ADMIN', 'TEACHER'],
|
||||
'timetable:manage': ['ADMIN'],
|
||||
'client:update': ['ADMIN'],
|
||||
'homework:read': ['ADMIN', 'TEACHER'],
|
||||
'homework:write': ['ADMIN', 'TEACHER'],
|
||||
'notification:read': ['ADMIN', 'TEACHER'],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { PermissionScope as ContractPermissionScope, ReleaseProgressDto as ContractReleaseProgressDto } from './class-plan.js';
|
||||
/**
|
||||
* 三端共享的领域类型定义。
|
||||
* 这里的类型是后端 REST API / WebSocket 契约的唯一来源,
|
||||
@@ -334,6 +335,7 @@ export interface TermWeekDto {
|
||||
}
|
||||
|
||||
export interface TermWeeksDto {
|
||||
maxTermWeeks?: number;
|
||||
/**
|
||||
* 这套区间属于谁:空串 = **全校默认**;非空 = 某个班的覆盖。
|
||||
* 班级没有自己的配置时回落到全校默认。
|
||||
@@ -503,6 +505,8 @@ export interface UpdateClassAccountRequest {
|
||||
/* ------------------------------------------------------------------ 课程 */
|
||||
|
||||
export interface CourseDto {
|
||||
initial?: string;
|
||||
isOutDoor?: boolean;
|
||||
id: string;
|
||||
name: string;
|
||||
teacherId: string;
|
||||
@@ -1181,6 +1185,7 @@ export interface DashboardSummary {
|
||||
|
||||
/** 服务端 -> 客户端事件名 */
|
||||
export interface ServerToClientEvents {
|
||||
'client:release-progress': (payload: ContractReleaseProgressDto) => void;
|
||||
'notification:deleted': (payload: { id: string; classId: string }) => void;
|
||||
'notification:new': (payload: NotificationDto) => void;
|
||||
/** 叫人:定向广播给被叫学生 */
|
||||
@@ -1422,6 +1427,8 @@ export interface UpdateInfo {
|
||||
* **没有任何分组**时回落角色默认模板(逐条镜像权限组上线前各路由的 requireRole)。
|
||||
*/
|
||||
export interface PermissionGroupDto {
|
||||
scope?: ContractPermissionScope;
|
||||
scopeValue?: string;
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
@@ -1462,12 +1469,16 @@ export interface PermissionCatalogDto {
|
||||
}
|
||||
|
||||
export interface CreatePermissionGroupRequest {
|
||||
scope?: ContractPermissionScope;
|
||||
scopeValue?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
keys?: PermissionKey[];
|
||||
}
|
||||
|
||||
export interface UpdatePermissionGroupRequest {
|
||||
scope?: ContractPermissionScope;
|
||||
scopeValue?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
keys?: PermissionKey[];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getMaxTermWeeks } from './class-plan.js';
|
||||
import { WEEKDAY_LABELS, WEEKDAYS, WEEK_PARITY_LABELS } from './constants.js';
|
||||
import type { ClassPeriod, GradeLevel, ScheduleDto, ScheduleWeekView, WeekParity } from './types.js';
|
||||
|
||||
@@ -291,6 +292,7 @@ export function timeToMinutes(value: string): number {
|
||||
}
|
||||
|
||||
const MS_PER_DAY = 86_400_000;
|
||||
/** @deprecated 仅保留旧调用方 ABI;新代码统一调用 getMaxTermWeeks。 */
|
||||
export const MAX_TERM_WEEK = 30;
|
||||
|
||||
/**
|
||||
@@ -300,7 +302,7 @@ export const MAX_TERM_WEEK = 30;
|
||||
export function resolveCurrentWeek(
|
||||
termStartDate: string,
|
||||
now: Date = new Date(),
|
||||
maxWeek: number = MAX_TERM_WEEK,
|
||||
maxWeek: number = getMaxTermWeeks(),
|
||||
): number {
|
||||
if (!termStartDate) return 1;
|
||||
const start = toDate(termStartDate);
|
||||
@@ -346,7 +348,7 @@ export function resolveWeekFromRanges(
|
||||
*/
|
||||
export function buildAutoTermWeeks(
|
||||
termStartDate: string,
|
||||
maxWeek: number = MAX_TERM_WEEK,
|
||||
maxWeek: number = getMaxTermWeeks(),
|
||||
): Array<{ weekNumber: number; startDate: string; endDate: string; note: string }> {
|
||||
if (!termStartDate) return [];
|
||||
const start = toDate(termStartDate);
|
||||
@@ -355,7 +357,7 @@ export function buildAutoTermWeeks(
|
||||
const monday = new Date(start.getTime() - (weekday - 1) * MS_PER_DAY);
|
||||
monday.setHours(0, 0, 0, 0);
|
||||
|
||||
const total = Math.max(1, Math.min(maxWeek, MAX_TERM_WEEK));
|
||||
const total = getMaxTermWeeks(maxWeek);
|
||||
const weeks: Array<{ weekNumber: number; startDate: string; endDate: string; note: string }> = [];
|
||||
for (let index = 0; index < total; index += 1) {
|
||||
const from = new Date(monday.getTime() + index * 7 * MS_PER_DAY);
|
||||
|
||||
Reference in new issue
Block a user