Version: 0.9.79.dev.260506

后端:
1. 本地后端启动体系收口到 `backend/scripts`,移除 `cmd/all` 聚合入口,并将仓库根兼容启动语义收敛为 `StartAPI` 别名;新增 dev-up / dev-down / services-up / services-down / dev-status / dev-logs / service-restart 脚本,统一托管多服务进程、日志、PID 与基础设施启动。
2. 课表服务超时口径统一放宽到 5 分钟,覆盖 gateway / client / rpc server / config example,避免课表导入与图片识别在长耗时场景下被内层提前截断。
3. `today` 课表查询修正为读取真实当前日期,不再使用硬编码测试日期;同时剔除旧缓存与返回结果里的 `empty` 占位事件,后端只返回真实日程,空档改由前端时间轴自行补齐。

前端:
4. 首页路由切回改为复用 `DashboardView` 实例,补 `keep-alive`、`onActivated` 与双帧缩放重算,修复从侧栏返回首页时首帧布局放大与重复加载闪动问题。
5. 首页加载态与今日时间线口径收口:移除额外 800ms `pageLoading` 人为延迟,task / schedule 改为分开驱动;时间线忽略 `empty` 事件,并统一空档文案为“无课”。
6. 收敛助手页与首页若干进场/弹性动画,降低结果卡片、微调弹窗、思考区与面板切换时的抖动感。

仓库:
7. README 补充后端本地快速启动说明,`.gitignore` 忽略 `backend/.dev` 脚本运行态产物。
This commit is contained in:
Losita
2026-05-06 12:59:29 +08:00
parent d4afc6ef74
commit 7d324b77aa
27 changed files with 1329 additions and 135 deletions

View File

@@ -52,7 +52,9 @@ router.afterEach(() => {
<MainSidebar />
<div class="smartmate-content">
<router-view v-slot="{ Component }">
<component :is="Component" />
<keep-alive include="DashboardView">
<component :is="Component" />
</keep-alive>
</router-view>
</div>
</div>

View File

@@ -772,14 +772,7 @@ const currentWeekEntries = computed(() =>
}
/* 进场动画 */
@keyframes board-item-spring {
0% { opacity: 0; transform: scale(0.6) translateY(20px); }
60% { opacity: 1; transform: scale(1.05) translateY(-2px); }
100% { opacity: 1; transform: scale(1) translateY(0); }
}
.board-item-pop {
animation: board-item-spring 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
/* 弹窗核心动画:采用物理弹簧质感 */
@@ -796,26 +789,4 @@ const currentWeekEntries = computed(() =>
opacity: 0;
}
.modal-enter-active .schedule-modal {
animation: modal-pop-in 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.modal-leave-active .schedule-modal {
animation: modal-pop-in 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) reverse;
}
@keyframes modal-pop-in {
0% {
transform: scale(0.9) translateY(40px);
opacity: 0;
}
60% {
transform: scale(1.02) translateY(-2px);
opacity: 1;
}
100% {
transform: scale(1) translateY(0);
opacity: 1;
}
}
</style>

View File

@@ -44,23 +44,11 @@ const emit = defineEmits<{
border: 1px solid rgba(15, 23, 42, 0.06);
border-radius: 20px;
cursor: pointer;
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
transition: all 0.2s ease;
margin: 12px 0;
position: relative;
overflow: hidden;
/* 弹出动画 */
animation: schedule-card-pop 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
@keyframes schedule-card-pop {
0% {
opacity: 0;
transform: scale(0.9) translateY(10px);
}
100% {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.schedule-result-card:hover {

View File

@@ -353,18 +353,6 @@ function formatDateLabel(value: string) {
display: grid;
gap: 14px;
/* 弹出动画 */
animation: planning-panel-pop 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
@keyframes planning-panel-pop {
0% {
opacity: 0;
transform: translateY(8px) scale(0.98);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.assistant-planning__panel-header strong {

View File

@@ -43,12 +43,6 @@ const recordData = computed(() => props.payload.data as TaskRecordCardData)
margin: 12px 0;
display: flex;
flex-direction: column;
animation: card-appear 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes card-appear {
0% { opacity: 0; transform: scale(0.95) translateY(10px); }
100% { opacity: 1; transform: scale(1) translateY(0); }
}
.unknown-card {

View File

@@ -4000,14 +4000,7 @@ onBeforeUnmount(() => {
</template>
<style scoped>
@keyframes assistant-item-pop {
0% { opacity: 0; transform: scale(0.98) translateY(10px); }
60% { opacity: 1; transform: scale(1.01) translateY(-1px); }
100% { opacity: 1; transform: scale(1) translateY(0); }
}
.dashboard-item-pop {
animation: assistant-item-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both;
animation-delay: var(--anim-delay, 0s);
}
@@ -4742,7 +4735,6 @@ onBeforeUnmount(() => {
position: relative;
overflow: hidden;
border-top: 4px solid #f59e0b; /* 警告色顶部装饰条 */
animation: confirm-card-enter 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.assistant-confirm-card__header {
@@ -5261,19 +5253,16 @@ onBeforeUnmount(() => {
/* 推理框展开收起弹性动效 */
.reasoning-bounce-enter-active {
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
transform-origin: top center;
transition: opacity 0.18s ease;
}
.reasoning-bounce-leave-active {
transition: all 0.2s ease;
transform-origin: top center;
transition: opacity 0.12s ease;
}
.reasoning-bounce-enter-from,
.reasoning-bounce-leave-to {
opacity: 0;
transform: translateY(-15px);
}
.chat-message__reasoning-title {
@@ -5826,11 +5815,6 @@ onBeforeUnmount(() => {
to { background-position: 0% 0; }
}
@keyframes confirm-card-enter {
0% { opacity: 0; transform: translateY(10px) scale(0.985); }
100% { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes pulse-dot {
0% { box-shadow: 0 0 0 0 rgba(90, 152, 255, 0.34); }
70% { box-shadow: 0 0 0 8px rgba(90, 152, 255, 0); }

View File

@@ -62,6 +62,7 @@ function buildTimeKey(start?: string | null, end?: string | null) {
const eventMap = computed(() => {
const map = new Map<string, TodayEvent>()
for (const event of props.events ?? []) {
if ((event.type || '').trim() === 'empty') continue
map.set(buildTimeKey(event.start_time, event.end_time), event)
}
return map
@@ -92,8 +93,8 @@ const renderSlots = computed<RenderSlot[]>(() =>
key: slot.key,
kind: 'event',
timeText: formatTimeRange(event?.start_time || slot.startTime, event?.end_time || slot.endTime),
title: event?.name || '今日无安排',
locationText: event?.location || '休息时间',
title: event?.name || '无课',
locationText: event?.location || '当前时段无课程安排',
tone: resolveCardTone(event),
}
}),

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRouter } from 'vue-router'
@@ -11,10 +11,13 @@ import { useAuthStore } from '@/stores/auth'
import type { TaskItem, TodayEvent } from '@/types/dashboard'
import { formatHeaderDate } from '@/utils/date'
defineOptions({
name: 'DashboardView',
})
const router = useRouter()
const authStore = useAuthStore()
const pageLoading = ref(true)
const taskLoading = ref(true)
const scheduleLoading = ref(true)
const saveTaskLoading = ref(false)
@@ -32,6 +35,8 @@ const dashboardMainScale = ref(1)
const tasks = ref<TaskItem[]>([])
const todayEvents = ref<TodayEvent[]>([])
let dashboardScaleAnimationFrame = 0
const taskForm = reactive<{
title: string
priority_group: number
@@ -110,10 +115,7 @@ async function loadScheduleData() {
}
async function loadDashboardData() {
pageLoading.value = true
const minLoadingTimer = new Promise((resolve) => setTimeout(resolve, 800))
await Promise.allSettled([loadTasksData(), loadScheduleData(), minLoadingTimer])
pageLoading.value = false
await Promise.allSettled([loadTasksData(), loadScheduleData()])
}
async function handleTaskToggle(task: TaskItem) {
@@ -228,31 +230,52 @@ function syncDashboardMainScale() {
const topbar = dashboardTopbarRef.value
const content = dashboardContentRef.value
if (!main || !inner || !topbar || !content || window.innerWidth <= 980) { dashboardMainScale.value = 1; return }
dashboardMainScale.value = 1
window.requestAnimationFrame(() => {
const availableHeight = main.clientHeight
const gridGap = 10
const naturalHeight = topbar.getBoundingClientRect().height + content.scrollHeight + gridGap
if (!availableHeight || !naturalHeight) { dashboardMainScale.value = 1; return }
const nextScale = Math.min(1, (availableHeight / naturalHeight) * 0.96)
dashboardMainScale.value = Number(nextScale.toFixed(4))
const availableHeight = main.clientHeight
const gridGap = 10
const naturalHeight = topbar.offsetHeight + content.scrollHeight + gridGap
if (!availableHeight || !naturalHeight) return
const nextScale = Number(Math.min(1, (availableHeight / naturalHeight) * 0.96).toFixed(4))
dashboardMainScale.value = nextScale
}
function scheduleDashboardMainScaleSync() {
if (typeof window === 'undefined') return
if (dashboardScaleAnimationFrame) window.cancelAnimationFrame(dashboardScaleAnimationFrame)
// 1. 侧栏切回首页时,外层布局会比首页内容晚一点稳定。
// 2. 延后两帧再测量,只处理“回首页首帧偏大”的问题,避免持续重算。
dashboardScaleAnimationFrame = window.requestAnimationFrame(() => {
dashboardScaleAnimationFrame = window.requestAnimationFrame(() => {
dashboardScaleAnimationFrame = 0
syncDashboardMainScale()
})
})
}
onMounted(async () => {
await nextTick()
scheduleDashboardMainScaleSync()
await loadDashboardData()
await nextTick()
syncDashboardMainScale()
window.addEventListener('resize', syncDashboardMainScale)
scheduleDashboardMainScaleSync()
window.addEventListener('resize', scheduleDashboardMainScaleSync)
})
onActivated(async () => {
await nextTick()
scheduleDashboardMainScaleSync()
})
onBeforeUnmount(() => {
window.removeEventListener('resize', syncDashboardMainScale)
if (dashboardScaleAnimationFrame) window.cancelAnimationFrame(dashboardScaleAnimationFrame)
window.removeEventListener('resize', scheduleDashboardMainScaleSync)
})
watch([() => tasks.value.length, () => todayEvents.value.length, pageLoading], async () => {
watch([() => tasks.value.length, () => todayEvents.value.length, taskLoading, scheduleLoading], async () => {
await nextTick()
syncDashboardMainScale()
scheduleDashboardMainScaleSync()
}, { flush: 'post' })
</script>
@@ -277,7 +300,7 @@ watch([() => tasks.value.length, () => todayEvents.value.length, pageLoading], a
</header>
<div ref="dashboardContentRef" class="dashboard-content page-shell">
<TodayTimeline :style="{ '--anim-delay': '0.04s' }" :events="todayEvents" :loading="scheduleLoading || pageLoading" />
<TodayTimeline :style="{ '--anim-delay': '0.04s' }" :events="todayEvents" :loading="scheduleLoading" />
<div class="dashboard-actions dashboard-item-pop" :style="{ '--anim-delay': '0.08s' }">
<button type="button" class="dashboard-actions__primary" @click="openCreateTaskDialog">添加任务</button>
@@ -295,7 +318,7 @@ watch([() => tasks.value.length, () => todayEvents.value.length, pageLoading], a
:empty-text="quadrantMeta[group].emptyText"
:count="groupedTasks[group].length"
:tasks="groupedTasks[group]"
:loading="taskLoading || pageLoading"
:loading="taskLoading"
@toggle="handleTaskToggle"
@edit="handleTaskEdit"
@delete="handleTaskDelete"