Version: 0.8.3.dev.260328

后端:
1.彻底删除原agent文件夹,并将现agent2文件夹全量重命名为agent(包括全部涉及到的文件以及文档、注释),迁移工作完美结束
2.修复了重试消息的相关逻辑问题

前端:
1.改善了一些交互体验,修复了一些bug,现在只剩少的功能了,现存的bug基本都修复完毕

全仓库:
1.更新了决策记录和README文档
This commit is contained in:
Losita
2026-03-28 18:00:31 +08:00
parent 5fc9548420
commit 468367d617
108 changed files with 1910 additions and 17173 deletions

View File

@@ -72,7 +72,6 @@ function resolveDetailPanelStyle(items: TaskClassDetail['items']) {
// 2. 条目超过“当前屏幕可安全展示的最大条数”后,立即锁住高度并进入内部滚动。
// 3. 这样像 8 条 task_item 这类中等长度列表会稳定触发滚动,不会再因为估算过大而失效。
return {
height: `${finalHeight}px`,
maxHeight: `${finalHeight}px`,
}
}
@@ -299,13 +298,15 @@ watch(
overflow-y: auto;
overflow-x: hidden;
padding: 24px;
display: grid;
align-content: start;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 14px;
scrollbar-gutter: stable;
}
.task-class-sidebar__skeleton-item {
flex: 0 0 auto;
height: 120px;
border-radius: 24px;
background: linear-gradient(90deg, rgba(234, 239, 246, 0.9), rgba(248, 251, 255, 1), rgba(234, 239, 246, 0.9));
@@ -314,6 +315,7 @@ watch(
}
.task-class-card {
flex: 0 0 auto;
min-width: 0;
border-radius: 24px;
border: 1px solid rgba(216, 225, 238, 0.9);
@@ -330,6 +332,7 @@ watch(
.task-class-card__summary {
width: 100%;
min-width: 0;
min-height: 92px;
border: none;
background: transparent;
padding: 18px 20px 18px 18px;
@@ -394,7 +397,9 @@ watch(
}
.task-class-card__detail {
box-sizing: border-box;
min-width: 0;
min-height: 0;
padding: 0 14px 14px;
overflow-y: auto;
overflow-x: hidden;
@@ -490,6 +495,7 @@ watch(
}
.task-class-sidebar__create {
flex: 0 0 auto;
min-width: 0;
min-height: 108px;
border: 1px dashed rgba(204, 216, 232, 0.92);
@@ -542,6 +548,7 @@ watch(
.task-class-card__summary {
padding: 16px 16px 16px 15px;
min-height: 84px;
}
}
@@ -590,6 +597,7 @@ watch(
.task-class-card__summary {
padding: 14px 14px 14px 13px;
min-height: 76px;
}
.task-class-card__content {
@@ -616,6 +624,7 @@ watch(
.task-class-card__summary {
padding: 12px;
min-height: 72px;
}
.task-class-card__corner {

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed } from 'vue'
import { computed, ref } from 'vue'
import type { ScheduleWeekData, ScheduleWeekEvent } from '@/types/schedule'
@@ -15,18 +15,31 @@ interface SectionSlot {
timeRange: string
}
interface PreviewMovePayload {
week: number
sourceDayOfWeek: number
sourceOrder: number
targetDayOfWeek: number
targetOrder: number
}
const props = defineProps<{
weekLabel: string
weekHeaders: WeekDayHeader[]
weekData: ScheduleWeekData | null
scheduleSelectionMode: boolean
selectedScheduleEventIds: number[]
previewDragEnabled: boolean
}>()
const emit = defineEmits<{
toggleScheduleEvent: [eventId: number]
movePreviewEvent: [payload: PreviewMovePayload]
}>()
const draggingCellKey = ref<string | null>(null)
const dragOverCellKey = ref<string | null>(null)
const sectionSlots: SectionSlot[] = [
{ order: 1, title: '1-2', timeRange: '08:00\n09:40' },
{ order: 2, title: '3-4', timeRange: '10:15\n11:55' },
@@ -54,11 +67,24 @@ function isSelected(eventId: number) {
return props.selectedScheduleEventIds.includes(eventId)
}
function hasEmbeddedTask(event?: ScheduleWeekEvent) {
return Boolean(
event &&
event.type === 'course' &&
event.embedded_task_info &&
event.embedded_task_info.id > 0,
)
}
function resolveEventTone(event?: ScheduleWeekEvent) {
if (!event || event.type === 'empty') {
return 'empty'
}
if (hasEmbeddedTask(event)) {
return 'course-embedded'
}
if (event.type === 'course') {
return 'course'
}
@@ -88,6 +114,149 @@ function resolveCellMeta(event?: ScheduleWeekEvent) {
}
return event.location || '未定'
}
function resolveEmbeddedTaskName(event?: ScheduleWeekEvent) {
if (!hasEmbeddedTask(event)) {
return ''
}
return event!.embedded_task_info.name
}
// isSuggestedPreviewEvent 负责判断当前格子是否允许作为“拖拽源”。
//
// 职责边界:
// 1. 这里只判断前端交互条件,不负责真正改写 preview JSON。
// 2. 只有 preview 模式下的 suggested 条目才允许拖拽,正式课表与普通课程保持只读。
function isSuggestedPreviewEvent(event?: ScheduleWeekEvent) {
return Boolean(
props.previewDragEnabled &&
!props.scheduleSelectionMode &&
event &&
event.status === 'suggested',
)
}
function isEmbeddedSuggestedPreviewEvent(event?: ScheduleWeekEvent) {
return Boolean(
isSuggestedPreviewEvent(event) &&
event &&
event.type === 'course' &&
hasEmbeddedTask(event),
)
}
function isWholeCellDraggable(event?: ScheduleWeekEvent) {
return Boolean(isSuggestedPreviewEvent(event) && !isEmbeddedSuggestedPreviewEvent(event))
}
// canDropPreviewEvent 负责判断当前格子是否允许作为“拖拽目标”。
//
// 设计说明:
// 1. 空白格允许放置 suggested 任务。
// 2. 课程格允许接收 suggested 任务,父组件会把它转换成“嵌入课程”的预览结构。
// 3. suggested 格本身也允许作为目标,用于交换两个建议任务的位置。
function canDropPreviewEvent(event?: ScheduleWeekEvent) {
if (!props.previewDragEnabled || props.scheduleSelectionMode) {
return false
}
if (!event || event.type === 'empty') {
return true
}
if (event.status === 'suggested') {
return true
}
return event.type === 'course'
}
function buildCellKey(dayOfWeek: number, order: number) {
return `${dayOfWeek}-${order}`
}
function handlePreviewDragStart(dayOfWeek: number, order: number, dragEvent: DragEvent) {
const event = resolveEvent(dayOfWeek, order)
if (!isSuggestedPreviewEvent(event) || !props.weekData) {
dragEvent.preventDefault()
return
}
draggingCellKey.value = buildCellKey(dayOfWeek, order)
dragOverCellKey.value = null
dragEvent.dataTransfer?.setData(
'application/json',
JSON.stringify({
week: props.weekData.week,
sourceDayOfWeek: dayOfWeek,
sourceOrder: order,
}),
)
if (dragEvent.dataTransfer) {
dragEvent.dataTransfer.effectAllowed = 'move'
}
}
function handlePreviewDragOver(dayOfWeek: number, order: number, dragEvent: DragEvent) {
if (!draggingCellKey.value) {
return
}
const cellKey = buildCellKey(dayOfWeek, order)
if (cellKey === draggingCellKey.value || !canDropPreviewEvent(resolveEvent(dayOfWeek, order))) {
return
}
dragEvent.preventDefault()
dragOverCellKey.value = cellKey
if (dragEvent.dataTransfer) {
dragEvent.dataTransfer.dropEffect = 'move'
}
}
function handlePreviewDrop(dayOfWeek: number, order: number, dragEvent: DragEvent) {
if (!draggingCellKey.value) {
return
}
const cellKey = buildCellKey(dayOfWeek, order)
const payloadText = dragEvent.dataTransfer?.getData('application/json')
if (!payloadText || cellKey === draggingCellKey.value || !canDropPreviewEvent(resolveEvent(dayOfWeek, order))) {
draggingCellKey.value = null
dragOverCellKey.value = null
return
}
try {
const payload = JSON.parse(payloadText) as Partial<PreviewMovePayload>
if (
typeof payload.week !== 'number' ||
typeof payload.sourceDayOfWeek !== 'number' ||
typeof payload.sourceOrder !== 'number'
) {
return
}
dragEvent.preventDefault()
emit('movePreviewEvent', {
week: payload.week,
sourceDayOfWeek: payload.sourceDayOfWeek,
sourceOrder: payload.sourceOrder,
targetDayOfWeek: dayOfWeek,
targetOrder: order,
})
} finally {
draggingCellKey.value = null
dragOverCellKey.value = null
}
}
function handlePreviewDragEnd() {
draggingCellKey.value = null
dragOverCellKey.value = null
}
</script>
<template>
@@ -119,8 +288,16 @@ function resolveCellMeta(event?: ScheduleWeekEvent) {
{
'planning-board__cell--selectable': scheduleSelectionMode && resolveEvent(header.dayOfWeek, slot.order)?.type !== 'empty',
'planning-board__cell--selected': resolveEvent(header.dayOfWeek, slot.order) && isSelected(resolveEvent(header.dayOfWeek, slot.order)!.id),
'planning-board__cell--draggable': isWholeCellDraggable(resolveEvent(header.dayOfWeek, slot.order)),
'planning-board__cell--dragging': draggingCellKey === buildCellKey(header.dayOfWeek, slot.order),
'planning-board__cell--dragover': dragOverCellKey === buildCellKey(header.dayOfWeek, slot.order),
},
]"
:draggable="isWholeCellDraggable(resolveEvent(header.dayOfWeek, slot.order))"
@dragstart="handlePreviewDragStart(header.dayOfWeek, slot.order, $event)"
@dragover="handlePreviewDragOver(header.dayOfWeek, slot.order, $event)"
@drop="handlePreviewDrop(header.dayOfWeek, slot.order, $event)"
@dragend="handlePreviewDragEnd"
>
<button
v-if="scheduleSelectionMode && resolveEvent(header.dayOfWeek, slot.order)?.type !== 'empty'"
@@ -131,7 +308,33 @@ function resolveCellMeta(event?: ScheduleWeekEvent) {
/>
<template v-if="resolveEvent(header.dayOfWeek, slot.order)">
<div class="planning-board__cell-main">
<div
v-if="hasEmbeddedTask(resolveEvent(header.dayOfWeek, slot.order))"
class="planning-board__embedded-shell"
>
<div class="planning-board__embedded-course">
<strong>{{ resolveCellTitle(resolveEvent(header.dayOfWeek, slot.order)) }}</strong>
</div>
<div class="planning-board__embedded-task">
<strong
class="planning-board__embedded-task-dragger"
:class="{
'planning-board__embedded-task-dragger--active': isEmbeddedSuggestedPreviewEvent(resolveEvent(header.dayOfWeek, slot.order)),
}"
:draggable="isEmbeddedSuggestedPreviewEvent(resolveEvent(header.dayOfWeek, slot.order))"
@dragstart.stop="handlePreviewDragStart(header.dayOfWeek, slot.order, $event)"
@dragend.stop="handlePreviewDragEnd"
>
{{ resolveEmbeddedTaskName(resolveEvent(header.dayOfWeek, slot.order)) }}
</strong>
</div>
</div>
<div
v-else
class="planning-board__cell-main"
>
<strong>{{ resolveCellTitle(resolveEvent(header.dayOfWeek, slot.order)) }}</strong>
<span>{{ resolveCellMeta(resolveEvent(header.dayOfWeek, slot.order)) }}</span>
</div>
@@ -261,11 +464,85 @@ function resolveCellMeta(event?: ScheduleWeekEvent) {
background: #acd6f4;
}
.planning-board__cell--course-embedded {
background: linear-gradient(180deg, rgba(121, 187, 239, 0.96) 0%, rgba(88, 161, 225, 0.96) 100%);
align-items: stretch;
padding: 9px;
}
.planning-board__cell--course .planning-board__cell-main strong,
.planning-board__cell--course .planning-board__cell-main span {
color: #2576cc;
}
.planning-board__embedded-shell {
display: grid;
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
gap: 8px;
width: 100%;
height: 100%;
min-height: 0;
text-align: center;
overflow: hidden;
}
.planning-board__embedded-course,
.planning-board__embedded-task {
display: flex;
align-items: center;
justify-content: center;
min-width: 0;
min-height: 0;
overflow: hidden;
}
.planning-board__embedded-course {
padding: 6px 4px;
color: #ffffff;
}
.planning-board__embedded-course strong,
.planning-board__embedded-task strong {
min-width: 0;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
white-space: normal;
overflow-wrap: anywhere;
text-align: center;
}
.planning-board__embedded-course strong {
width: 100%;
font-size: 13px;
line-height: 1.28;
font-weight: 800;
-webkit-line-clamp: 2;
}
.planning-board__embedded-task {
padding: 6px 8px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 10px 18px rgba(31, 82, 145, 0.14);
}
.planning-board__embedded-task strong {
color: #1f5db3;
font-size: 11px;
line-height: 1.24;
font-weight: 800;
-webkit-line-clamp: 2;
}
.planning-board__embedded-task-dragger {
width: 100%;
}
.planning-board__embedded-task-dragger--active {
cursor: grab;
}
.planning-board__cell--amber {
background: #ffe58b;
}
@@ -319,6 +596,20 @@ function resolveCellMeta(event?: ScheduleWeekEvent) {
box-shadow: inset 0 0 0 2px rgba(32, 102, 212, 0.52);
}
.planning-board__cell--draggable {
cursor: grab;
}
.planning-board__cell--dragging {
opacity: 0.42;
}
.planning-board__cell--dragover {
box-shadow:
inset 0 0 0 2px rgba(20, 92, 192, 0.58),
0 0 0 4px rgba(33, 109, 215, 0.1);
}
.planning-board__checkbox {
position: absolute;
top: 16px;
@@ -378,6 +669,14 @@ function resolveCellMeta(event?: ScheduleWeekEvent) {
.planning-board__cell-main strong {
font-size: 14px;
}
.planning-board__embedded-course strong {
font-size: 12px;
}
.planning-board__cell--course-embedded {
padding: 8px;
}
}
@media (max-width: 1180px) {
@@ -433,5 +732,23 @@ function resolveCellMeta(event?: ScheduleWeekEvent) {
.planning-board__cell {
padding: 12px 8px;
}
.planning-board__embedded-task {
padding: 5px 7px;
border-radius: 12px;
}
.planning-board__embedded-course {
padding: 4px 2px;
}
.planning-board__embedded-course strong,
.planning-board__embedded-task strong {
font-size: 11px;
}
.planning-board__cell--course-embedded {
padding: 7px;
}
}
</style>