feat: 重做灵动岛编辑器与课表档案时间表编辑
灵动岛 - 重构自定义方案编辑器:组件库常驻、拖拽添加与排序、方案切换/新建/复制/删除、编辑模式预览与缩放,容器在编辑时展开全部子组件。 - 新增天气简报组件:主进程固定请求 Open-Meteo,只接受数值经纬度,失败退避并保留本次运行缓存;冒烟模式不访问外网。 - 倒计时支持目标名称与日期;课程表组件新增「今日时间点」,按时间点类型折叠/展开,课中不再常驻倒计时。 - preload 暴露 getIslandWeather 与时间点订阅,契约同步 desktop.d.ts。 课表档案 - 时间点默认课程:布局返回 defaultCourse,日课表没有条目时回退;新增 apply-default 覆盖所有引用课表;全校时间表不携带默认课程。 - 新增 ClassIsland 式时间表编辑器(列表/时间轴视图、拖动把柄、按类型添加、默认时长与缩放)与「课表信息」表单。 - 课表页默认进入单表编辑,底部保留列表/周/月视图;时间表支持新路由删除。 - 「课间」统一改称「课间休息」。 验证 - pnpm typecheck、pnpm lint 通过。 - verify:e2e 隔离验收 248/248 项通过(含默认课程回退、覆盖写入与时间表删除)。 - ui-smoke 覆盖单表编辑、课表信息与时间轴视图,并更新回归截图。
This commit is contained in:
1 parent
938285bb9c
commit
682818dd4a
41 files changed
+4349
-1095
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import type { ClassPeriod as ContractClassPeriod } from './types.js';
|
||||
import type { ScheduleDto } from './types.js';
|
||||
import type { CourseBrief, ScheduleDto } from './types.js';
|
||||
|
||||
export const TIME_POINT_TYPES = ['LESSON', 'BREAK', 'DIVIDER', 'ACTION'] as const;
|
||||
export type TimePointType = (typeof TIME_POINT_TYPES)[number];
|
||||
@@ -12,6 +12,8 @@ export interface TimePointDto {
|
||||
timeType: TimePointType;
|
||||
isHideDefault: boolean;
|
||||
defaultClassId: string | null;
|
||||
/** 服务端投影出的默认课程;日课表没有单独条目时用它回退。 */
|
||||
defaultCourse?: CourseBrief | null;
|
||||
breakName: string;
|
||||
actionSet: string;
|
||||
}
|
||||
@@ -25,6 +27,8 @@ export interface DailyPlanEntryDto {
|
||||
id: string;
|
||||
timeLayoutItemId: string;
|
||||
courseId: string | null;
|
||||
/** 服务端 include 的课程摘要;课表页据此显示科目。 */
|
||||
course?: CourseBrief | null;
|
||||
isEnabled: boolean;
|
||||
isChangedClass: boolean;
|
||||
location: string | null;
|
||||
@@ -60,6 +64,25 @@ export interface ActiveClassPlanWeekDto {
|
||||
days: Array<ActiveClassPlanDto & { weekday: number; date: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 灵动岛当天时间点。
|
||||
*
|
||||
* 自定义常驻岛的「课程表」组件按它渲染 ClassIsland 主界面式的折叠/展开时间点:
|
||||
* 上课点折叠只显示课程名,当前点展开显示时间段;课间休息与默认隐藏的时间点只在轮到时出现。
|
||||
*/
|
||||
export interface IslandTimePointDto {
|
||||
id: string;
|
||||
timeType: TimePointType;
|
||||
/** 课间用 breakName,上课用时间点名称 */
|
||||
name: string;
|
||||
courseName: string | null;
|
||||
/** HH:mm */
|
||||
startTime: string;
|
||||
/** HH:mm */
|
||||
endTime: string;
|
||||
isHideDefault: boolean;
|
||||
}
|
||||
|
||||
/** 设置来自服务端;离线/未初始化时统一回落 30。 */
|
||||
export function getMaxTermWeeks(configured?: unknown): number {
|
||||
const value = Number(configured);
|
||||
|
||||
@@ -507,7 +507,7 @@ export const CLASSISLAND_TIME_TYPES = {
|
||||
*/
|
||||
export const CLASSISLAND_TIME_TYPE_LABELS: Record<number, string> = {
|
||||
0: '上课',
|
||||
1: '课间',
|
||||
1: '课间休息',
|
||||
2: '分割线',
|
||||
3: '行动',
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { isDayKey } from './utils.js';
|
||||
|
||||
export const ISLAND_COMPONENTS = [
|
||||
'date',
|
||||
'clock',
|
||||
'weather',
|
||||
'countdown',
|
||||
'schedule',
|
||||
'text',
|
||||
@@ -32,17 +35,25 @@ export const CLASSISLAND_ISLAND_METRICS = {
|
||||
fadedOpacity: 0.05,
|
||||
} as const;
|
||||
export type IslandComponentKind = (typeof ISLAND_COMPONENTS)[number];
|
||||
export interface IslandWeather {
|
||||
temperature: number;
|
||||
code: number;
|
||||
windSpeed: number;
|
||||
fetchedAt: number;
|
||||
stale: boolean;
|
||||
}
|
||||
export const ISLAND_COMPONENT_LABELS: Record<IslandComponentKind, string> = {
|
||||
date: '日期',
|
||||
clock: '时钟',
|
||||
weather: '天气简报',
|
||||
countdown: '倒计时',
|
||||
schedule: '课程表',
|
||||
text: '文本',
|
||||
separator: '分隔符',
|
||||
slide: '轮播',
|
||||
rolling: '滚动',
|
||||
group: '分组',
|
||||
stack: '堆叠',
|
||||
separator: '分割线',
|
||||
slide: '轮播容器',
|
||||
rolling: '滚动容器',
|
||||
group: '分组容器',
|
||||
stack: '堆叠容器',
|
||||
notification: '通知',
|
||||
homework: '作业',
|
||||
call: '叫人',
|
||||
@@ -71,7 +82,12 @@ export interface IslandComponentNode {
|
||||
template: boolean;
|
||||
hour12: boolean;
|
||||
seconds: boolean;
|
||||
target: 'auto' | 'next' | 'end' | 'school';
|
||||
target: 'auto' | 'next' | 'end' | 'school' | 'date';
|
||||
targetDate: string;
|
||||
countdownTitle: string;
|
||||
locationName: string;
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
display: 'current' | 'next' | 'today';
|
||||
count: number;
|
||||
showPeriod: boolean;
|
||||
@@ -165,6 +181,11 @@ export function islandNode(kind: IslandComponentKind, id: string): IslandCompone
|
||||
hour12: false,
|
||||
seconds: true,
|
||||
target: 'auto',
|
||||
targetDate: '',
|
||||
countdownTitle: '目标日',
|
||||
locationName: '',
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
display: 'current',
|
||||
count: 5,
|
||||
showPeriod: true,
|
||||
@@ -260,7 +281,18 @@ export function normalizeIslandLayouts(input: unknown): IslandLayoutSettings {
|
||||
template: s.template === true,
|
||||
hour12: s.hour12 === true,
|
||||
seconds: s.seconds !== false,
|
||||
target: ['auto', 'next', 'end', 'school'].includes(s.target) ? s.target : 'auto',
|
||||
target: ['auto', 'next', 'end', 'school', 'date'].includes(s.target) ? s.target : 'auto',
|
||||
targetDate: typeof s.targetDate === 'string' && isDayKey(s.targetDate) ? s.targetDate : '',
|
||||
countdownTitle: String(s.countdownTitle || '目标日').slice(0, 64),
|
||||
locationName: String(s.locationName || '').slice(0, 64),
|
||||
latitude:
|
||||
typeof s.latitude === 'number' && Number.isFinite(s.latitude)
|
||||
? clamp(s.latitude, -90, 90, 0)
|
||||
: null,
|
||||
longitude:
|
||||
typeof s.longitude === 'number' && Number.isFinite(s.longitude)
|
||||
? clamp(s.longitude, -180, 180, 0)
|
||||
: null,
|
||||
display: ['current', 'next', 'today'].includes(s.display) ? s.display : 'current',
|
||||
count: clamp(s.count, 1, 40, 5),
|
||||
showPeriod: s.showPeriod !== false,
|
||||
|
||||
Reference in new issue
Block a user