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:
LoveChengke committed 2026-10-25 16:41:29 +08:00
1 parent 938285bb9c
commit 682818dd4a
41 files changed
+4349 -1095

No files matched your search

@@ -23,6 +23,14 @@ const paths: Record<string, string> = {
shield: 'M12 3l8 3v6c0 5-8 9-8 9s-8-4-8-9V6z M8 12l3 3 5-6',
back: 'M11 5l-7 7 7 7 M4 12h13a3 3 0 0 1 3 3v3',
chevron: 'M6 9l6 6 6-6',
lesson: 'M4 5h16v12H4z M8 21h8 M12 17v4 M7 9h6 M7 12h4',
break: 'M6 8h9v4a4 4 0 0 1-4 4H10a4 4 0 0 1-4-4V8z M15 9h2a2 2 0 0 1 0 4h-2 M4 20h13',
divider: 'M4 12h16',
action: 'M13 3 6 13h5l-1 8 8-11h-5l1-7z',
timeline: 'M7 3v18 M4 6h6 M4 12h6 M4 18h6 M14 8h6v4h-6z M14 15h4v4h-4z',
trash: 'M4 7h16 M9 7V4h6v3 M6 7l1 14h10l1-14 M10 11v6 M14 11v6',
'zoom-in': 'M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14 M20 20l-4-4 M8 11h6 M11 8v6',
'zoom-out': 'M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14 M20 20l-4-4 M8 11h6',
};
</script>
@@ -45,7 +45,7 @@ const CLASS_PLAN_SAMPLE = `{
"Name": "周一作息",
"Layouts": [
{ "StartTime": "08:00:00", "EndTime": "08:45:00", "TimeType": 0, "BreakName": "" },
{ "StartTime": "08:45:00", "EndTime": "08:55:00", "TimeType": 1, "BreakName": "课间" },
{ "StartTime": "08:45:00", "EndTime": "08:55:00", "TimeType": 1, "BreakName": "课间休息" },
{ "StartTime": "08:55:00", "EndTime": "09:40:00", "TimeType": 0, "BreakName": "" }
]
}
@@ -0,0 +1,153 @@
<script setup lang="ts">
import type { ClassPlanDto, TimeLayoutDetailDto } from '@classhelper/shared';
import ArchiveIcon from './ArchiveIcon.vue';
/**
* 日课表的触发规则与基本信息。
*
* 同一份表单同时用于课表页右侧「课表信息」标签和周视图的详细信息入口,
* 直接修改父组件传入的 `plan`,保留 PlanEditorView 的脏数据比较。
*/
const plan = defineModel<ClassPlanDto>('plan', { required: true });
const props = defineProps<{
layouts: TimeLayoutDetailDto[];
groups: { id: string; name: string; isGlobal: boolean; isDefault: boolean }[];
maxWeek: number;
disabled: boolean;
saving: boolean;
}>();
const emit = defineEmits<{
'change-layout': [value: string];
'edit-layout': [];
save: [];
}>();
</script>
<template>
<el-form :disabled="disabled" label-position="top" class="plan-details" @submit.prevent>
<label class="detail-field">
<span>课表名称</span>
<el-input v-model="plan.name" maxlength="64" placeholder="例如:星期一课表" />
</label>
<label class="detail-field">
<span>启用星期</span>
<el-select v-model="plan.weekday">
<el-option label="任意星期" :value="0" />
<el-option v-for="day in 7" :key="day" :value="day" :label="`星期${'一二三四五六日'[day - 1]}`" />
</el-select>
</label>
<div class="detail-field">
<span>轮周设定</span>
<div class="inline-fields">
<el-input-number v-model="plan.weekCountDivTotal" :min="1" :max="9" />
<span>周一轮,使用第</span>
<el-input-number v-model="plan.weekCountDiv" :min="0" :max="plan.weekCountDivTotal" />
<span>周</span>
</div>
<small>轮换周期第 0 周表示每周使用,支持 1–9 周轮换。</small>
</div>
<div class="detail-field">
<span>生效周次</span>
<div class="inline-fields">
<el-input-number v-model="plan.weekStart" :min="1" :max="plan.weekEnd" />
<span>至</span>
<el-input-number v-model="plan.weekEnd" :min="plan.weekStart" :max="props.maxWeek" />
<span>周</span>
</div>
</div>
<div class="detail-field">
<span>时间表模板</span>
<el-select
:model-value="plan.timeLayoutId"
placeholder="选择时间表"
@change="emit('change-layout', $event)"
>
<el-option v-for="item in props.layouts" :key="item.id" :value="item.id" :label="item.name" />
</el-select>
<button class="text-command" type="button" @click="emit('edit-layout')">
<ArchiveIcon name="clock" />
编辑时间表
</button>
</div>
<div class="detail-field">
<span>课表群</span>
<el-select v-model="plan.associatedGroup" clearable placeholder="未分组">
<el-option v-for="group in props.groups" :key="group.id" :value="group.id" :label="group.name" />
</el-select>
</div>
<label class="detail-field inline">
<span>启用课表</span>
<el-switch v-model="plan.isEnabled" />
</label>
<el-button
type="primary"
class="save-plan"
:disabled="props.disabled"
:loading="props.saving"
@click="emit('save')"
>
保存课表
</el-button>
</el-form>
</template>
<style scoped>
.plan-details {
display: flex;
flex-direction: column;
gap: 12px;
}
.detail-field {
display: flex;
flex-direction: column;
gap: 6px;
color: var(--ch-text-secondary);
font-size: var(--el-font-size-small);
}
.detail-field :deep(.el-select),
.detail-field :deep(.el-input) {
width: 100%;
}
.detail-field small {
color: var(--ch-text-muted);
font-size: var(--el-font-size-extra-small);
line-height: 1.4;
}
.detail-field.inline {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.inline-fields {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
}
.inline-fields :deep(.el-input-number) {
width: 74px;
}
.inline-fields span {
white-space: nowrap;
}
.text-command {
display: inline-flex;
align-items: center;
align-self: flex-start;
gap: 5px;
padding: 4px 0;
border: 0;
background: transparent;
color: var(--el-color-primary);
font: inherit;
cursor: pointer;
}
.text-command .archive-icon {
width: 16px;
height: 16px;
}
.save-plan {
margin-top: 4px;
width: 100%;
}
</style>
File diff suppressed because it is too large. Load diff
@@ -243,7 +243,7 @@ function fillSample(): void {
<el-table-column prop="endTime" label="结束" width="90" />
<el-table-column label="类型" width="100">
<template #default="{ row }">
{{ row.type === 'break' ? '课间' : row.type === 'divider' ? '分割线' : '上课' }}
{{ row.type === 'break' ? '课间休息' : row.type === 'divider' ? '分割线' : '上课' }}
</template>
</el-table-column>
<el-table-column label="跳过" width="80">
File diff suppressed because it is too large. Load diff