|
|
|
|
@ -1,15 +1,60 @@
|
|
|
|
|
<template>
|
|
|
|
|
<aside :class="['training-ai-sidebar', { 'is-collapsed': sidebarCollapsed }]">
|
|
|
|
|
<!-- 收起态:右下角圆形悬浮球 -->
|
|
|
|
|
<!-- 收起态:右下角 3D 助手形象 + 文字胶囊悬浮按钮 -->
|
|
|
|
|
<button
|
|
|
|
|
v-if="sidebarCollapsed"
|
|
|
|
|
class="ai-fab"
|
|
|
|
|
:class="['ai-fab', { 'is-hidden': fabHidden }]"
|
|
|
|
|
type="button"
|
|
|
|
|
aria-label="展开 AI 助学与助评"
|
|
|
|
|
title="AI 助学助手"
|
|
|
|
|
aria-label="展开 AI 学习助手"
|
|
|
|
|
title="AI 学习助手"
|
|
|
|
|
@click="sidebarCollapsed = false"
|
|
|
|
|
>
|
|
|
|
|
<el-icon class="ai-fab__icon"><MagicStick /></el-icon>
|
|
|
|
|
<span class="ai-fab__mascot" aria-hidden="true">
|
|
|
|
|
<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
|
|
|
|
<defs>
|
|
|
|
|
<linearGradient id="aiFabBody" x1="0" y1="0" x2="0" y2="1">
|
|
|
|
|
<stop offset="0" stop-color="#8ab6ff" />
|
|
|
|
|
<stop offset="0.55" stop-color="#3b82f6" />
|
|
|
|
|
<stop offset="1" stop-color="#1d4ed8" />
|
|
|
|
|
</linearGradient>
|
|
|
|
|
<linearGradient id="aiFabVisor" x1="0" y1="0" x2="0" y2="1">
|
|
|
|
|
<stop offset="0" stop-color="#1e3a8a" />
|
|
|
|
|
<stop offset="1" stop-color="#152757" />
|
|
|
|
|
</linearGradient>
|
|
|
|
|
<linearGradient id="aiFabTip" x1="0" y1="0" x2="0" y2="1">
|
|
|
|
|
<stop offset="0" stop-color="#fde68a" />
|
|
|
|
|
<stop offset="1" stop-color="#f59e0b" />
|
|
|
|
|
</linearGradient>
|
|
|
|
|
</defs>
|
|
|
|
|
<!-- 地面投影 -->
|
|
|
|
|
<ellipse cx="32" cy="59" rx="15" ry="3" fill="#1e3a8a" opacity="0.2" />
|
|
|
|
|
<!-- 天线 + 顶珠 -->
|
|
|
|
|
<line x1="32" y1="13" x2="32" y2="7" stroke="#1e40af" stroke-width="2.4" stroke-linecap="round" />
|
|
|
|
|
<circle cx="32" cy="5.5" r="3.2" fill="url(#aiFabTip)" />
|
|
|
|
|
<!-- 侧耳 -->
|
|
|
|
|
<rect x="7" y="28" width="7" height="12" rx="3.5" fill="#1e40af" />
|
|
|
|
|
<rect x="50" y="28" width="7" height="12" rx="3.5" fill="#1e40af" />
|
|
|
|
|
<!-- 3D 黏土头部 -->
|
|
|
|
|
<ellipse cx="32" cy="34" rx="20" ry="19" fill="url(#aiFabBody)" />
|
|
|
|
|
<!-- 高光 -->
|
|
|
|
|
<ellipse cx="24" cy="21" rx="7.5" ry="3.6" fill="#ffffff" opacity="0.35" transform="rotate(-16 24 21)" />
|
|
|
|
|
<!-- 显示器面罩 -->
|
|
|
|
|
<rect x="17" y="25" width="30" height="15" rx="7.5" fill="url(#aiFabVisor)" />
|
|
|
|
|
<!-- 发光眼睛 -->
|
|
|
|
|
<circle cx="26.5" cy="32.5" r="2.7" fill="#7dd3fc" />
|
|
|
|
|
<circle cx="37.5" cy="32.5" r="2.7" fill="#7dd3fc" />
|
|
|
|
|
<circle cx="27.3" cy="31.7" r="0.9" fill="#ffffff" />
|
|
|
|
|
<circle cx="38.3" cy="31.7" r="0.9" fill="#ffffff" />
|
|
|
|
|
<!-- 微笑 -->
|
|
|
|
|
<path d="M29 36.5 Q32 39 35 36.5" stroke="#7dd3fc" stroke-width="1.6" fill="none" stroke-linecap="round" />
|
|
|
|
|
<!-- 星光点缀 -->
|
|
|
|
|
<path d="M50 12 C50.7 15.3 53.4 16.3 56 17 C53.4 17.7 50.7 18.7 50 22 C49.3 18.7 46.6 17.7 44 17 C46.6 16.3 49.3 15.3 50 12 Z" fill="#fbbf24" />
|
|
|
|
|
</svg>
|
|
|
|
|
</span>
|
|
|
|
|
<span class="ai-fab__label">
|
|
|
|
|
<strong>AI 学习助手</strong>
|
|
|
|
|
<em>学习有疑问?随时问我</em>
|
|
|
|
|
</span>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<!-- 展开态 -->
|
|
|
|
|
@ -219,6 +264,25 @@ const sidebarCollapsed = ref(true);
|
|
|
|
|
const activePanel = ref("study");
|
|
|
|
|
const chatStreamRef = ref(null);
|
|
|
|
|
const assistInputRef = ref(null);
|
|
|
|
|
|
|
|
|
|
/* 悬浮按钮滚动方向感知:向下滚动时隐藏(避免遮挡底部导航/内容),向上滚动时滑出 */
|
|
|
|
|
const fabHidden = ref(false);
|
|
|
|
|
let lastScrollY = 0;
|
|
|
|
|
let fabScrollTicking = false;
|
|
|
|
|
function handleWindowScroll() {
|
|
|
|
|
if (fabScrollTicking) return;
|
|
|
|
|
fabScrollTicking = true;
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
fabScrollTicking = false;
|
|
|
|
|
const y = window.scrollY || 0;
|
|
|
|
|
const delta = y - lastScrollY;
|
|
|
|
|
if (Math.abs(delta) < 6) return;
|
|
|
|
|
fabHidden.value = delta > 0 && y > 40;
|
|
|
|
|
lastScrollY = y;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
onMounted(() => window.addEventListener("scroll", handleWindowScroll, { passive: true }));
|
|
|
|
|
onBeforeUnmount(() => window.removeEventListener("scroll", handleWindowScroll));
|
|
|
|
|
/** 输入框最大高度(约 6 行):到此停止增高,内容在框内滚动(DeepSeek 式) */
|
|
|
|
|
const ASSIST_INPUT_MAX_HEIGHT = 168;
|
|
|
|
|
function autoGrowAssist() {
|
|
|
|
|
@ -410,29 +474,159 @@ function plainText(value) {
|
|
|
|
|
width: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ===== 收起态:右下角圆形悬浮球 ===== */
|
|
|
|
|
/* ===== 收起态:待机仅 3D 头像,悬停像扇子一样展开文字 ===== */
|
|
|
|
|
.ai-fab {
|
|
|
|
|
position: fixed;
|
|
|
|
|
right: 24px;
|
|
|
|
|
bottom: 28px;
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
width: 50px;
|
|
|
|
|
height: 50px;
|
|
|
|
|
border: 0;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
padding: 4px;
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0);
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
background: var(--ai-accent);
|
|
|
|
|
box-shadow: 0 8px 24px rgba(37, 99, 235, 0.32);
|
|
|
|
|
background: transparent;
|
|
|
|
|
box-shadow: 0 8px 22px rgba(37, 99, 235, 0.32);
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
text-align: left;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: background 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
|
|
|
|
|
transition: transform 0.22s ease, box-shadow 0.22s ease, opacity 0.22s ease,
|
|
|
|
|
border-color 0.25s ease, padding 0.28s ease;
|
|
|
|
|
}
|
|
|
|
|
.ai-fab__icon { font-size: 23px; color: #ffffff; }
|
|
|
|
|
.ai-fab:hover {
|
|
|
|
|
background: var(--ai-accent-deep);
|
|
|
|
|
box-shadow: 0 10px 28px rgba(37, 99, 235, 0.4);
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
|
|
|
|
|
/* 胶囊底色:默认透明,悬停时淡入,避免渐变背景无法过渡的问题 */
|
|
|
|
|
.ai-fab::before {
|
|
|
|
|
content: "";
|
|
|
|
|
position: absolute;
|
|
|
|
|
inset: 0;
|
|
|
|
|
border-radius: inherit;
|
|
|
|
|
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 55%, #1d4ed8 100%);
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transition: opacity 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 展开态:悬停 / 键盘聚焦(慢速展开,带 ease-out 回弹感) */
|
|
|
|
|
.ai-fab:hover:not(.is-hidden),
|
|
|
|
|
.ai-fab:focus-visible:not(.is-hidden) {
|
|
|
|
|
padding: 4px 20px 4px 4px;
|
|
|
|
|
border-color: rgba(255, 255, 255, 0.55);
|
|
|
|
|
box-shadow: 0 14px 30px rgba(37, 99, 235, 0.46);
|
|
|
|
|
transition: padding 0.5s cubic-bezier(0.22, 1, 0.36, 1),
|
|
|
|
|
border-color 0.4s ease, box-shadow 0.35s ease,
|
|
|
|
|
transform 0.22s ease, opacity 0.22s ease;
|
|
|
|
|
}
|
|
|
|
|
.ai-fab:hover:not(.is-hidden)::before,
|
|
|
|
|
.ai-fab:focus-visible:not(.is-hidden)::before {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
transition: opacity 0.45s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 向下滚动时自动隐藏:滑出视口底部,完全不遮挡内容 */
|
|
|
|
|
.ai-fab.is-hidden {
|
|
|
|
|
transform: translateY(150%);
|
|
|
|
|
opacity: 0;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
.ai-fab:hover:not(.is-hidden) {
|
|
|
|
|
transform: translateY(-3px);
|
|
|
|
|
}
|
|
|
|
|
.ai-fab:active { transform: translateY(-1px); }
|
|
|
|
|
|
|
|
|
|
/* 3D 助手头像:浅色圆底衬托 SVG 形象,缓慢上下浮动 */
|
|
|
|
|
.ai-fab__mascot {
|
|
|
|
|
position: relative;
|
|
|
|
|
z-index: 1;
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
width: 44px;
|
|
|
|
|
height: 44px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: radial-gradient(circle at 30% 25%, #ffffff 0%, #dbeafe 62%, #bfdbfe 100%);
|
|
|
|
|
box-shadow: inset 0 -2px 5px rgba(30, 58, 138, 0.18), 0 2px 6px rgba(15, 23, 42, 0.22);
|
|
|
|
|
animation: mascot-bob 3.2s ease-in-out infinite;
|
|
|
|
|
}
|
|
|
|
|
.ai-fab__mascot svg {
|
|
|
|
|
display: block;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 呼吸光环:吸引注意但不刺眼 */
|
|
|
|
|
.ai-fab__mascot::before {
|
|
|
|
|
content: "";
|
|
|
|
|
position: absolute;
|
|
|
|
|
inset: -4px;
|
|
|
|
|
border: 2px solid rgba(147, 197, 253, 0.75);
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
animation: mascot-pulse 2.4s ease-out infinite;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 文字描述:默认折叠在头像后方,悬停时像扇子一样横向展开 */
|
|
|
|
|
.ai-fab__label {
|
|
|
|
|
position: relative;
|
|
|
|
|
z-index: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 1px;
|
|
|
|
|
max-width: 0;
|
|
|
|
|
margin-left: 0;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transform: translateX(-10px) scale(0.96);
|
|
|
|
|
transform-origin: left center;
|
|
|
|
|
/* 收合速度(离开悬停时稍快回收) */
|
|
|
|
|
transition: max-width 0.32s ease-in, margin-left 0.32s ease-in, opacity 0.2s ease-in,
|
|
|
|
|
transform 0.32s ease-in;
|
|
|
|
|
|
|
|
|
|
strong {
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
letter-spacing: 0.5px;
|
|
|
|
|
line-height: 18px;
|
|
|
|
|
}
|
|
|
|
|
em {
|
|
|
|
|
color: rgba(255, 255, 255, 0.78);
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
font-style: normal;
|
|
|
|
|
line-height: 15px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.ai-fab:hover:not(.is-hidden) .ai-fab__label,
|
|
|
|
|
.ai-fab:focus-visible:not(.is-hidden) .ai-fab__label {
|
|
|
|
|
max-width: 180px;
|
|
|
|
|
margin-left: 10px;
|
|
|
|
|
opacity: 1;
|
|
|
|
|
transform: translateX(0) scale(1);
|
|
|
|
|
/* 展开速度(放慢 + ease-out,扇子徐徐展开) */
|
|
|
|
|
transition: max-width 0.55s cubic-bezier(0.22, 1, 0.36, 1),
|
|
|
|
|
margin-left 0.55s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.4s ease-out 0.08s,
|
|
|
|
|
transform 0.55s cubic-bezier(0.22, 1, 0.36, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 悬停时小助手挥手式摆动 */
|
|
|
|
|
.ai-fab:hover .ai-fab__mascot svg { animation: mascot-wiggle 0.55s ease; }
|
|
|
|
|
|
|
|
|
|
@keyframes mascot-bob {
|
|
|
|
|
0%, 100% { transform: translateY(0); }
|
|
|
|
|
50% { transform: translateY(-3px); }
|
|
|
|
|
}
|
|
|
|
|
@keyframes mascot-pulse {
|
|
|
|
|
0% { transform: scale(0.85); opacity: 0.9; }
|
|
|
|
|
70%, 100% { transform: scale(1.35); opacity: 0; }
|
|
|
|
|
}
|
|
|
|
|
@keyframes mascot-wiggle {
|
|
|
|
|
0%, 100% { transform: rotate(0); }
|
|
|
|
|
25% { transform: rotate(-7deg); }
|
|
|
|
|
60% { transform: rotate(6deg); }
|
|
|
|
|
}
|
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
|
|
|
.ai-fab:not(.is-hidden),
|
|
|
|
|
.ai-fab:hover:not(.is-hidden) { transform: none; }
|
|
|
|
|
.ai-fab__mascot,
|
|
|
|
|
.ai-fab__mascot::before,
|
|
|
|
|
.ai-fab:hover .ai-fab__mascot svg { animation: none; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ===== 展开态骨架:白底卡片,撑满 fixed 根 ===== */
|
|
|
|
|
|