Version: 0.7.7.dev.260325
前端: 对主页做了一些改进,但是依然存在许多问题
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -5,10 +5,11 @@ import type { TodayEvent } from '@/types/dashboard'
|
||||
import { formatTimeRange } from '@/utils/date'
|
||||
|
||||
interface TimelineSlot {
|
||||
order: number
|
||||
key: string
|
||||
kind: 'event' | 'pause'
|
||||
label: string
|
||||
timeText?: string
|
||||
eventOrder?: number
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -17,13 +18,17 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const slotBlueprint: TimelineSlot[] = [
|
||||
{ order: 1, kind: 'event', label: '上午' },
|
||||
{ order: 2, kind: 'event', label: '上午' },
|
||||
{ order: 3, kind: 'pause', label: '午休', timeText: '11:55 - 14:00' },
|
||||
{ order: 4, kind: 'event', label: '下午' },
|
||||
{ order: 5, kind: 'event', label: '下午' },
|
||||
{ order: 6, kind: 'pause', label: '晚餐', timeText: '17:55 - 19:00' },
|
||||
{ order: 7, kind: 'event', label: '晚间' },
|
||||
{ key: 'slot-1', kind: 'event', label: '上午', timeText: '08:00 - 09:40', eventOrder: 1 },
|
||||
{ key: 'slot-2', kind: 'event', label: '上午', timeText: '10:15 - 11:55', eventOrder: 2 },
|
||||
{ key: 'slot-noon', kind: 'pause', label: '午休' },
|
||||
{ key: 'slot-4', kind: 'event', label: '下午', timeText: '14:00 - 15:40', eventOrder: 4 },
|
||||
// 1. 晚餐块固定放在 7-8 节与 9-10 节之间,作为晚间课程前的过渡占位。
|
||||
// 2. 根据用户最新要求,它要出现在“17:55 结束的课块之后、19:00 黄色块之前”。
|
||||
// 3. 用户要求该块只保留单独卡片,不展示时间文本。
|
||||
{ key: 'slot-dinner', kind: 'pause', label: '晚餐' },
|
||||
{ key: 'slot-5', kind: 'event', label: '下午', timeText: '16:15 - 17:55', eventOrder: 5 },
|
||||
{ key: 'slot-6', kind: 'event', label: '晚间', timeText: '19:00 - 20:40', eventOrder: 6 },
|
||||
{ key: 'slot-7', kind: 'event', label: '晚间', timeText: '20:50 - 22:30', eventOrder: 7 },
|
||||
]
|
||||
|
||||
const eventMap = computed(() => {
|
||||
@@ -49,6 +54,13 @@ function resolveCardTone(event: TodayEvent) {
|
||||
|
||||
return orderToneMap[event.order] ?? 'neutral'
|
||||
}
|
||||
|
||||
function resolveSlotEvent(slot: TimelineSlot) {
|
||||
if (typeof slot.eventOrder !== 'number') {
|
||||
return null
|
||||
}
|
||||
return eventMap.value.get(slot.eventOrder) ?? null
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -62,34 +74,40 @@ function resolveCardTone(event: TodayEvent) {
|
||||
</header>
|
||||
|
||||
<div v-if="loading" class="timeline-skeleton">
|
||||
<div v-for="index in 7" :key="index" class="timeline-skeleton__item" />
|
||||
<div v-for="slot in slotBlueprint" :key="slot.key" class="timeline-skeleton__item" />
|
||||
</div>
|
||||
|
||||
<div v-else class="timeline-grid">
|
||||
<template v-for="slot in slotBlueprint" :key="slot.order">
|
||||
<template v-for="slot in slotBlueprint" :key="slot.key">
|
||||
<article v-if="slot.kind === 'pause'" class="timeline-placeholder timeline-placeholder--pause">
|
||||
<span v-if="slot.timeText" class="timeline-placeholder__time">{{ slot.timeText }}</span>
|
||||
<strong class="timeline-placeholder__title">{{ slot.label }}</strong>
|
||||
<span class="timeline-placeholder__hint">为中段留出缓冲与恢复时间</span>
|
||||
</article>
|
||||
|
||||
<article
|
||||
v-if="eventMap.has(slot.order)"
|
||||
v-else-if="resolveSlotEvent(slot)"
|
||||
class="timeline-event"
|
||||
:class="`timeline-event--${resolveCardTone(eventMap.get(slot.order)!)}`"
|
||||
:class="`timeline-event--${resolveCardTone(resolveSlotEvent(slot)!)}`"
|
||||
>
|
||||
<span class="timeline-event__time">
|
||||
{{
|
||||
formatTimeRange(
|
||||
eventMap.get(slot.order)?.start_time,
|
||||
eventMap.get(slot.order)?.end_time,
|
||||
resolveSlotEvent(slot)?.start_time,
|
||||
resolveSlotEvent(slot)?.end_time,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<strong class="timeline-event__title">{{ eventMap.get(slot.order)?.name }}</strong>
|
||||
<strong class="timeline-event__title">{{ resolveSlotEvent(slot)?.name }}</strong>
|
||||
<span class="timeline-event__location">
|
||||
{{ eventMap.get(slot.order)?.location || '休息时间' }}
|
||||
{{ resolveSlotEvent(slot)?.location || '休息时间' }}
|
||||
</span>
|
||||
</article>
|
||||
|
||||
<article v-else class="timeline-placeholder timeline-placeholder--pause">
|
||||
<span class="timeline-placeholder__time">{{ slot.timeText }}</span>
|
||||
<strong class="timeline-placeholder__title">{{ slot.label }}</strong>
|
||||
<span class="timeline-placeholder__hint">为中段留出缓冲与恢复时间</span>
|
||||
<article v-else class="timeline-event timeline-event--neutral">
|
||||
<span class="timeline-event__time">{{ slot.timeText }}</span>
|
||||
<strong class="timeline-event__title">无课</strong>
|
||||
<span class="timeline-event__location">休息时间</span>
|
||||
</article>
|
||||
</template>
|
||||
</div>
|
||||
@@ -98,6 +116,7 @@ function resolveCardTone(event: TodayEvent) {
|
||||
|
||||
<style scoped>
|
||||
.timeline-card {
|
||||
min-width: 0;
|
||||
border-radius: 28px;
|
||||
padding: 22px 22px 20px;
|
||||
border: 1px solid rgba(17, 24, 39, 0.08);
|
||||
@@ -133,16 +152,20 @@ function resolveCardTone(event: TodayEvent) {
|
||||
}
|
||||
|
||||
.timeline-grid {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(132px, 1fr));
|
||||
/* 1. 改为 auto-fit 自适应列数,避免固定列数把左侧主区整体撑宽。 */
|
||||
/* 2. 每张卡片保留可读最小宽度,空间不足时自动换行,而不是出现横向滚动条。 */
|
||||
/* 3. 这样在左右近似二分的布局下,左侧信息板也能保持完整可见。 */
|
||||
grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
|
||||
gap: 12px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 4px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.timeline-event,
|
||||
.timeline-placeholder,
|
||||
.timeline-skeleton__item {
|
||||
min-width: 0;
|
||||
min-height: 124px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
@@ -226,6 +249,14 @@ function resolveCardTone(event: TodayEvent) {
|
||||
background: #57b8ea;
|
||||
}
|
||||
|
||||
.timeline-event--neutral {
|
||||
background: linear-gradient(180deg, #f8fbff 0%, #f3f7fc 100%);
|
||||
}
|
||||
|
||||
.timeline-event--neutral::before {
|
||||
background: #c8d6e8;
|
||||
}
|
||||
|
||||
.timeline-placeholder {
|
||||
border: 1px dashed rgba(120, 144, 171, 0.28);
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
@@ -260,8 +291,9 @@ function resolveCardTone(event: TodayEvent) {
|
||||
}
|
||||
|
||||
.timeline-skeleton {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(132px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@@ -280,4 +312,25 @@ function resolveCardTone(event: TodayEvent) {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1320px) {
|
||||
.timeline-grid,
|
||||
.timeline-skeleton {
|
||||
grid-template-columns: repeat(auto-fit, minmax(148px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1040px) {
|
||||
.timeline-card__header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 780px) {
|
||||
.timeline-grid,
|
||||
.timeline-skeleton {
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,11 +9,17 @@ function escapeHtml(input: string) {
|
||||
|
||||
function parseInlineMarkdown(input: string) {
|
||||
const inlineCodeBlocks: string[] = []
|
||||
let content = escapeHtml(input)
|
||||
const htmlBreakToken = '@@HTML_BREAK@@'
|
||||
let content = input.replace(/<br\s*\/?>/gi, htmlBreakToken)
|
||||
|
||||
// 1. 先抽离行内代码,避免代码片段里的 Markdown / HTML 被后续规则误处理。
|
||||
// 2. <br> 只做白名单放行,其它原始 HTML 仍统一转义,避免把模型输出直接注入页面。
|
||||
// 3. 若用户就是想输入普通换行,外层段落逻辑仍会继续按 <br /> 渲染,不受这里影响。
|
||||
content = escapeHtml(content)
|
||||
|
||||
content = content.replace(/`([^`]+)`/g, (_, code: string) => {
|
||||
const token = `@@INLINE_CODE_${inlineCodeBlocks.length}@@`
|
||||
inlineCodeBlocks.push(`<code>${escapeHtml(code)}</code>`)
|
||||
inlineCodeBlocks.push(`<code>${escapeHtml(code.replaceAll(htmlBreakToken, '<br>'))}</code>`)
|
||||
return token
|
||||
})
|
||||
|
||||
@@ -26,6 +32,7 @@ function parseInlineMarkdown(input: string) {
|
||||
content = content.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
||||
content = content.replace(/\*([^*]+)\*/g, '<em>$1</em>')
|
||||
content = content.replace(/~~([^~]+)~~/g, '<del>$1</del>')
|
||||
content = content.replaceAll(htmlBreakToken, '<br />')
|
||||
|
||||
return content.replace(/@@INLINE_CODE_(\d+)@@/g, (_, index: string) => inlineCodeBlocks[Number(index)] ?? '')
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ const tasks = ref<TaskItem[]>([])
|
||||
const todayEvents = ref<TodayEvent[]>([])
|
||||
|
||||
const sidebarWidth = ref(78)
|
||||
const assistantWidth = ref(460)
|
||||
const assistantWidth = ref(560)
|
||||
|
||||
const taskForm = reactive<{
|
||||
title: string
|
||||
@@ -221,8 +221,38 @@ function clampSidebarWidth(nextWidth: number) {
|
||||
return Math.min(110, Math.max(68, nextWidth))
|
||||
}
|
||||
|
||||
function clampAssistantWidth(nextWidth: number) {
|
||||
return Math.min(680, Math.max(380, nextWidth))
|
||||
function getAssistantWidthBounds(containerWidth: number, nextSidebarWidth = sidebarWidth.value) {
|
||||
// 1. 右侧助手区默认按“主区 / 助手区”近似二分来算,贴近用户给出的 DeepSeek 参考布局。
|
||||
// 2. 只允许在平衡宽度附近做小范围拖拽,避免主区被挤压后卡片内容大面积隐藏。
|
||||
// 3. 若窗口过窄,则仍保留主区最小可读宽度,优先保证左侧任务与日程信息可见。
|
||||
const reservedWidth = nextSidebarWidth + 20 + 32
|
||||
const availableWidth = Math.max(960, containerWidth - reservedWidth)
|
||||
const balancedWidth = availableWidth / 2
|
||||
const dragAllowance = Math.min(72, availableWidth * 0.08)
|
||||
const minWidth = Math.max(440, balancedWidth - dragAllowance)
|
||||
const maxWidth = Math.max(minWidth, Math.min(760, balancedWidth + dragAllowance))
|
||||
|
||||
return {
|
||||
balancedWidth,
|
||||
minWidth,
|
||||
maxWidth,
|
||||
}
|
||||
}
|
||||
|
||||
function clampAssistantWidth(nextWidth: number, containerWidth = dashboardLayoutRef.value?.getBoundingClientRect().width ?? 1600) {
|
||||
const { minWidth, maxWidth } = getAssistantWidthBounds(containerWidth)
|
||||
return Math.min(maxWidth, Math.max(minWidth, nextWidth))
|
||||
}
|
||||
|
||||
function syncAssistantWidthToBalancedSplit() {
|
||||
const layout = dashboardLayoutRef.value
|
||||
if (!layout || window.innerWidth <= 1380) {
|
||||
return
|
||||
}
|
||||
|
||||
const containerWidth = layout.getBoundingClientRect().width
|
||||
const { balancedWidth } = getAssistantWidthBounds(containerWidth)
|
||||
assistantWidth.value = clampAssistantWidth(balancedWidth, containerWidth)
|
||||
}
|
||||
|
||||
function startResize(type: 'sidebar' | 'assistant', event: PointerEvent) {
|
||||
@@ -242,7 +272,7 @@ function startResize(type: 'sidebar' | 'assistant', event: PointerEvent) {
|
||||
const handlePointerMove = (moveEvent: PointerEvent) => {
|
||||
const deltaX = moveEvent.clientX - startX
|
||||
const splitterTotalWidth = 20
|
||||
const minMainWidth = 760
|
||||
const minMainWidth = 560
|
||||
|
||||
if (type === 'sidebar') {
|
||||
const nextSidebarWidth = clampSidebarWidth(startSidebarWidth + deltaX)
|
||||
@@ -251,9 +281,9 @@ function startResize(type: 'sidebar' | 'assistant', event: PointerEvent) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextAssistantWidth = clampAssistantWidth(startAssistantWidth - deltaX)
|
||||
const maxAssistantWidth = rect.width - sidebarWidth.value - splitterTotalWidth - minMainWidth
|
||||
assistantWidth.value = Math.min(nextAssistantWidth, Math.max(380, maxAssistantWidth))
|
||||
const nextAssistantWidth = clampAssistantWidth(startAssistantWidth - deltaX, rect.width)
|
||||
assistantWidth.value = Math.min(nextAssistantWidth, Math.max(440, maxAssistantWidth))
|
||||
}
|
||||
|
||||
const stopResize = () => {
|
||||
@@ -269,10 +299,13 @@ function startResize(type: 'sidebar' | 'assistant', event: PointerEvent) {
|
||||
|
||||
onMounted(async () => {
|
||||
await loadDashboardData()
|
||||
syncAssistantWidthToBalancedSplit()
|
||||
window.addEventListener('resize', syncAssistantWidthToBalancedSplit)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.body.classList.remove('dashboard-resizing')
|
||||
window.removeEventListener('resize', syncAssistantWidthToBalancedSplit)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -538,6 +571,7 @@ onBeforeUnmount(() => {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dashboard-topbar {
|
||||
@@ -607,10 +641,13 @@ onBeforeUnmount(() => {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
overflow: auto;
|
||||
padding-right: 4px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding-right: 0;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.dashboard-actions {
|
||||
@@ -630,8 +667,9 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.dashboard-quadrants {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
@@ -718,6 +756,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.dashboard-assistant {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
align-self: stretch;
|
||||
@@ -733,7 +772,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
@media (max-width: 1640px) {
|
||||
.dashboard-layout {
|
||||
--dashboard-assistant-width: 430px;
|
||||
--dashboard-assistant-width: 520px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user