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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user