You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dianshang-qianduan/src/views/components/TrainingAiSidebar.vue

851 lines
26 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<aside :class="['training-ai-sidebar', { 'is-collapsed': sidebarCollapsed }]">
<!-- 收起态:右下角圆形悬浮球 -->
<button
v-if="sidebarCollapsed"
class="ai-fab"
type="button"
aria-label="展开 AI 助学与助评"
title="AI 助学助手"
@click="sidebarCollapsed = false"
>
<el-icon class="ai-fab__icon"><MagicStick /></el-icon>
</button>
<!-- 展开态 -->
<div v-else class="ai-sidebar-body">
<header class="ai-head">
<span class="ai-head__brand">
<el-icon><MagicStick /></el-icon>
AI 学习助手
</span>
<button
class="ai-head__close"
type="button"
aria-label="收起 AI 助学与助评"
title="收起"
@click="sidebarCollapsed = true"
>
<span aria-hidden="true">›</span>
</button>
</header>
<div class="ai-segmented" role="tablist">
<button
type="button"
role="tab"
:class="['ai-segmented__item', { on: activePanel === 'study' }]"
:aria-selected="String(activePanel === 'study')"
@click="activePanel = 'study'"
>
<el-icon><ChatDotRound /></el-icon>AI 助学
</button>
<button
type="button"
role="tab"
:class="['ai-segmented__item', { on: activePanel === 'review' }]"
:aria-selected="String(activePanel === 'review')"
@click="activePanel = 'review'"
>
<el-icon><DataAnalysis /></el-icon>AI 助评
</button>
</div>
<!-- ========== AI 助学 ========== -->
<div v-show="activePanel === 'study'" class="ai-pane">
<div ref="chatStreamRef" class="chat-stream" aria-live="polite">
<div v-if="!assistMessages.length" class="chat-line chat-line--ai">
<span class="chat-avatar">AI</span>
<p class="chat-text chat-text--ai">{{ displayAssistMessage }}</p>
</div>
<template v-for="message in assistMessages" :key="message.id">
<div v-if="message.role === 'student'" class="chat-line chat-line--user">
<p class="chat-text chat-text--user">{{ message.content }}</p>
</div>
<div v-else class="chat-line chat-line--ai">
<span class="chat-avatar">AI</span>
<p class="chat-text chat-text--ai">{{ message.content }}</p>
</div>
</template>
<div v-if="assistLoading" class="chat-line chat-line--ai">
<span class="chat-avatar">AI</span>
<p class="chat-thinking">
AI 正在思考
<span class="thinking-dots" aria-hidden="true"><i></i><i></i><i></i></span>
</p>
</div>
</div>
<button v-if="referenceText" type="button" class="reference-card" @click="useReferencePrompt">
<span class="reference-card__title">
<el-icon><Reading /></el-icon>{{ referenceTitle }}
</span>
<span class="reference-card__text" v-html="referenceText"></span>
</button>
<div class="composer">
<textarea
ref="assistInputRef"
v-model="assistQuestion"
placeholder="输入问题,Enter 发送 / Shift+Enter 换行"
rows="1"
@input="autoGrowAssist"
@keydown.enter.exact.prevent="requestAssist"
></textarea>
<button
type="button"
class="composer__send"
aria-label="发送助学问题"
:disabled="assistLoading"
@click="requestAssist"
>
<el-icon><Top /></el-icon>
</button>
</div>
</div>
<!-- ========== AI 助评 ========== -->
<div v-show="activePanel === 'review'" class="ai-pane">
<div v-if="displayScoreVisible" class="score-row">
<span class="score-row__label">综合评分</span>
<span class="score-row__value">
<strong>{{ displayFinalScore }}</strong><em>/ 100</em>
</span>
</div>
<div class="review-scroll">
<div v-if="hasStructuredAssessment" class="review-result">
<p v-if="reports.assessmentSummary" class="review-summary">{{ reports.assessmentSummary }}</p>
<div v-if="displayScoreCriteria.length" class="criteria" aria-label="评分明细">
<div v-for="item in displayScoreCriteria" :key="`${item.name}-${item.score}`" class="criterion">
<div class="criterion__top">
<span class="criterion__name">{{ item.name }}</span>
<span v-if="item.score !== ''" class="criterion__score">
{{ item.score }}<em v-if="item.maxScore !== ''"> / {{ item.maxScore }}</em>
</span>
</div>
<p v-if="item.description" class="criterion__desc">{{ item.description }}</p>
</div>
</div>
<div v-if="reports.improvements.length" class="improvements">
<span class="improvements__title">优先改进</span>
<ul>
<li v-for="item in reports.improvements" :key="item">{{ item }}</li>
</ul>
</div>
</div>
<div v-else class="review-empty">
<el-icon><DataLine /></el-icon>
<p>{{ displayEvalMessage }}</p>
</div>
</div>
<p v-if="currentTaskKey && !hasSavedAnswer" class="review-hint">请先至少填写并保存一项实训内容,再请求 AI 助评。</p>
<p v-else-if="currentTaskKey" class="review-hint">修改并保存答案后可重新助评;答案未变时保留上次结果。</p>
<button
type="button"
class="review-primary"
:disabled="!currentTaskKey || !hasSavedAnswer || assessmentLoading"
@click="requestEvaluation"
>
{{ assessmentLoading ? "AI 评分中…" : reports.assessment.status === "SUCCEEDED" ? "重新助评" : "AI 智能评分" }}
</button>
</div>
</div>
</aside>
</template>
<script setup>
import { ChatDotRound, DataAnalysis, DataLine, MagicStick, Reading, Top } from "@element-plus/icons-vue";
import { getAiTrainingEvaluation, requestAiTrainingAssessment, requestAiTrainingHelp } from "@/api/aiTrainingEvaluation";
import { normalizeEvaluationReports } from "./trainingAiEvaluationReport";
import { resolveTrainingTaskKey } from "@/views/training/taskKeyMap";
import { buildReferencePrompt } from "./studentTrainingAiPanel";
const props = defineProps({
studyTip: {
type: String,
default: "当前任务建议:先明确任务要求,再结合真实业务场景逐步补全实训内容。",
},
referenceTitle: {
type: String,
default: "填写提醒",
},
referenceText: {
type: String,
default: "",
},
assistText: {
type: String,
default: "建议已生成:请围绕任务目标补充关键数据、分析过程和结论依据。",
},
evalMessage: {
type: String,
default: "完成实训内容后,可以请求AI助评获取智能评分和改进建议。",
},
finalScore: {
type: [String, Number],
default: "--",
},
scoreVisible: {
type: Boolean,
default: false,
},
progressItems: {
type: Array,
default: () => [],
},
taskKey: { type: String, default: "" },
hasSavedAnswer: { type: Boolean, default: true },
});
const emit = defineEmits(["request-assist", "request-evaluation"]);
const assistQuestion = ref("");
const reviewQuestion = ref("");
const assistMessage = ref("");
const internalEvalMessage = ref("");
const internalScore = ref("--");
const internalScoreVisible = ref(false);
const assessmentLoading = ref(false);
const assistLoading = ref(false);
const evaluation = ref({});
const assistMessages = ref([]);
const sidebarCollapsed = ref(true);
const activePanel = ref("study");
const chatStreamRef = ref(null);
const assistInputRef = ref(null);
/** 输入框最大高度(约 6 行):到此停止增高,内容在框内滚动(DeepSeek 式) */
const ASSIST_INPUT_MAX_HEIGHT = 168;
function autoGrowAssist() {
const el = assistInputRef.value;
if (!el) return;
el.style.height = "auto";
el.style.height = `${Math.min(el.scrollHeight, ASSIST_INPUT_MAX_HEIGHT)}px`;
}
const route = useRoute();
const currentTaskKey = computed(() => resolveTrainingTaskKey(route, props.taskKey));
const reports = computed(() => normalizeEvaluationReports(evaluation.value));
const currentStepContext = computed(() => [props.studyTip, props.referenceTitle, plainText(props.referenceText)].filter(Boolean).join("\n"));
const displayScoreCriteria = computed(() => {
const criteria = reports.value.scoreCriteria;
const stepNames = props.progressItems.map((item) => String(item?.text || "").trim()).filter(Boolean);
if (!stepNames.length || stepNames.length !== criteria.length) return criteria;
return criteria.map((item, index) => ({ ...item, name: stepNames[index] }));
});
const displayAssistMessage = computed(() => assistMessage.value || props.studyTip);
const displayScoreVisible = computed(() => props.scoreVisible || internalScoreVisible.value || reports.value.score !== null);
const displayFinalScore = computed(() => (props.scoreVisible ? props.finalScore : reports.value.score ?? internalScore.value));
const hasStructuredAssessment = computed(() => Boolean(reports.value.assessmentSummary || displayScoreCriteria.value.length || reports.value.improvements.length));
const displayEvalMessage = computed(() => {
if (props.scoreVisible) return props.evalMessage;
return internalEvalMessage.value || props.evalMessage;
});
onMounted(loadEvaluation);
watch(currentTaskKey, (nextTaskKey, previousTaskKey) => {
if (nextTaskKey !== previousTaskKey) assistMessages.value = [];
loadEvaluation();
});
/* 新消息/思考态出现时,对话流自动滚到底部 */
watch([assistMessages, assistLoading], async () => {
await nextTick();
if (chatStreamRef.value) chatStreamRef.value.scrollTop = chatStreamRef.value.scrollHeight;
});
/* 输入框按内容自适应增高:覆盖参考提示词填充、发送清空两种程序化变更 */
watch(assistQuestion, async () => {
await nextTick();
autoGrowAssist();
});
/* 面板展开后 textarea 重新挂载,初始校准一次 */
watch(sidebarCollapsed, async (collapsed) => {
if (!collapsed) {
await nextTick();
autoGrowAssist();
}
});
async function loadEvaluation() {
if (!currentTaskKey.value) return;
try { evaluation.value = (await getAiTrainingEvaluation(currentTaskKey.value))?.data || {}; } catch (error) { evaluation.value = {}; }
}
function useReferencePrompt() {
assistQuestion.value = buildReferencePrompt(props.referenceText);
activePanel.value = "study";
}
async function requestAssist() {
emit("request-assist", assistQuestion.value);
const question = assistQuestion.value.trim();
if (!question) {
assistMessage.value = "请输入问题,或点击上方参考提示词后发送。";
return;
}
if (currentTaskKey.value && !props.hasSavedAnswer) {
assistMessage.value = "请先至少填写并保存一项实训内容,再请求 AI 助学。";
return;
}
assistMessages.value.push({ id: `student-${Date.now()}`, role: "student", content: question });
assistQuestion.value = "";
if (currentTaskKey.value) {
assistLoading.value = true;
try {
await requestAiTrainingHelp(currentTaskKey.value, question, currentStepContext.value);
await loadEvaluation();
assistMessage.value = reports.value.helpReport || props.assistText;
assistMessages.value.push({ id: `assistant-${Date.now()}`, role: "assistant", content: assistMessage.value });
} catch (error) {
assistMessages.value.push({ id: `assistant-${Date.now()}`, role: "assistant", content: "AI 助学暂时不可用,请稍后重试。" });
} finally {
assistLoading.value = false;
}
return;
}
assistMessage.value = `围绕“${question}”,建议先对照任务目标检查信息是否完整,再补充数据依据和分析结论。`;
assistMessages.value.push({ id: `assistant-${Date.now()}`, role: "assistant", content: assistMessage.value });
}
async function requestEvaluation() {
emit("request-evaluation", reviewQuestion.value);
activePanel.value = "review";
if (!currentTaskKey.value || !props.hasSavedAnswer) {
internalEvalMessage.value = "请先至少填写并保存一项实训内容,再请求 AI 助评。";
return;
}
assessmentLoading.value = true;
try {
await requestAiTrainingAssessment(currentTaskKey.value);
await loadEvaluation();
internalScoreVisible.value = reports.value.score !== null;
internalScore.value = reports.value.score ?? "--";
internalEvalMessage.value = reports.value.assessmentSummary || "AI 评分已生成,请查看评分明细与改进建议。";
} catch (error) {
internalEvalMessage.value = "AI 助评暂时不可用,请稍后重试。";
} finally {
assessmentLoading.value = false;
}
}
function plainText(value) {
return String(value || "")
.replace(/<[^>]*>/g, " ")
.replace(/&nbsp;/g, " ")
.replace(/\s+/g, " ")
.trim();
}
</script>
<style lang="scss" scoped>
/*
* 组件自带「墨黑极简」局部令牌(不依赖页面级 var(--student-*) 令牌),
* 保证在所有实训页(含未做墨黑换肤的页面)视觉一致。
* 宽度/定位不在这里设置——由全局 student-training.scss 的 shell grid 控制。
*
* 字体规范(对齐大厂悬浮面板):
* - Claude 设计指南:三级字号(标题/正文/标注)+ 两种字重(常规/强调)
* - Fluent 2(Copilot 侧栏):Caption 12/16、Body 14/20、Title3 24/32
* - Material 3 / Ant Design 5:正文锚点 14px,标注 12px
* - 中文按 M3「中高语言」类别行高上调,正文 14/22(≈1.6)
*/
.training-ai-sidebar {
--ai-accent: #2563eb;
--ai-accent-deep: #1d4ed8;
--ai-gray: #6b7280;
--ai-gray-light: #9ca3af;
--ai-soft: #f3f4f6;
--ai-soft-2: #fafbfc;
--ai-border: #e5e7eb;
--ai-border-strong: #d1d5db;
--ai-text: #1f2937;
--ai-text-2: #4b5563;
--ai-success: #4a9d6e;
/* 字体栈:跟随系统,中文优先苹方/雅黑 */
--ai-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue",
Helvetica, Arial, sans-serif;
/* 三级字号 */
--ai-fs-caption: 12px; /* 标注:提示、元信息、空状态、分段、标签 */
--ai-fs-body: 14px; /* 正文:对话、输入器、列表、摘要、按钮 */
--ai-fs-display: 24px; /* 展示:综合评分(Fluent Title 3) */
/* 行高 */
--ai-lh-caption: 18px; /* 1.5 */
--ai-lh-body: 22px; /* ≈1.57,中文阅读 */
/* 两种字重 */
--ai-fw-regular: 400;
--ai-fw-emphasized: 600;
font-family: var(--ai-font-family);
/* Web 悬浮面板标准:开启字体抗锯齿(ChatGPT/Copilot 同款渲染) */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* 真正悬浮:脱离文档流,不占用页面宽度与 grid 列 */
position: fixed;
z-index: 1001;
top: 64px;
right: 16px;
bottom: 16px;
width: min(585px, calc(100vw - 32px));
/* 根透明:不铺任何背景色,白底仅限卡片本身 */
background: transparent;
display: flex;
flex-direction: column;
}
/* 收起态:根不占任何空间,由悬浮球自行定位 */
.training-ai-sidebar.is-collapsed {
top: auto;
right: auto;
bottom: auto;
width: auto;
}
/* ===== 收起态:右下角圆形悬浮球 ===== */
.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%;
color: #ffffff;
background: var(--ai-accent);
box-shadow: 0 8px 24px rgba(37, 99, 235, 0.32);
cursor: pointer;
transition: background 0.16s ease, box-shadow 0.16s ease, transform 0.16s 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);
}
/* ===== 展开态骨架:白底卡片,撑满 fixed 根 ===== */
.ai-sidebar-body {
display: flex;
flex: 1 1 auto;
flex-direction: column;
min-height: 0;
border: 1px solid var(--ai-border);
border-radius: 12px;
background: #ffffff;
/* 柔和小投影,避免在页面底色上形成灰色边缘带 */
box-shadow: 0 6px 20px rgba(17, 24, 39, 0.12);
overflow: hidden;
}
.ai-head {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
padding: 0 8px 0 12px;
border-bottom: 1px solid var(--ai-border);
}
.ai-head__brand {
display: inline-flex;
align-items: center;
gap: 7px;
color: var(--ai-text);
font-size: var(--ai-fs-body);
font-weight: var(--ai-fw-emphasized);
.el-icon { color: var(--ai-accent); font-size: var(--student-fs-h1); }
}
.ai-head__close {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: 0;
border-radius: 7px;
color: var(--ai-gray);
background: transparent;
cursor: pointer;
span { font-size: 18px; line-height: 1; }
&:hover { color: var(--ai-accent); background: var(--ai-soft); }
}
/* 分段控件:助学 / 助评 */
.ai-segmented {
display: flex;
gap: 4px;
padding: 10px 12px 0;
}
.ai-segmented__item {
display: inline-flex;
flex: 1 1 0;
align-items: center;
justify-content: center;
gap: 5px;
height: 32px;
border: 1px solid transparent;
border-radius: 8px;
color: var(--ai-gray);
background: transparent;
font-family: inherit;
font-size: var(--ai-fs-caption);
font-weight: var(--ai-fw-emphasized);
cursor: pointer;
transition: color 0.16s ease, background 0.16s ease, border-color 0.16s ease;
.el-icon { font-size: var(--student-fs-body); }
&:hover { color: var(--ai-text); background: var(--ai-soft); }
&.on {
color: #ffffff;
background: var(--ai-accent);
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.22);
}
}
.ai-pane {
display: flex;
flex: 1 1 auto;
flex-direction: column;
min-height: 0;
padding: 4px 12px 12px;
}
/* ===== 助学:对话流(AI 去气泡 / 用户浅灰气泡) ===== */
.chat-stream {
display: flex;
flex: 1 1 auto;
flex-direction: column;
gap: 12px;
min-height: 140px;
padding: 12px 2px;
overflow-y: auto;
}
.chat-line { display: flex; margin: 0; }
.chat-line--ai { align-items: flex-start; gap: 8px; }
.chat-line--user { justify-content: flex-end; }
.chat-avatar {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
margin-top: 1px;
border-radius: 5px;
color: #ffffff;
background: var(--ai-accent);
font-size: 10px;
font-weight: var(--ai-fw-emphasized);
}
.chat-text {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
}
.chat-text--ai {
color: var(--ai-text);
font-size: var(--ai-fs-body);
line-height: var(--ai-lh-body);
}
.chat-text--user {
max-width: 82%;
padding: 8px 11px;
border-radius: 10px;
color: var(--ai-text);
background: var(--ai-soft);
font-size: var(--ai-fs-body);
line-height: var(--ai-lh-body);
}
/* 思考态:AI 正在思考 + 跳动蓝点 */
.chat-thinking {
display: flex;
align-items: center;
margin: 2px 0 0;
color: var(--ai-gray);
font-size: var(--ai-fs-caption);
line-height: 1;
}
.thinking-dots {
display: inline-flex;
gap: 4px;
margin-left: 6px;
i {
width: 5px;
height: 5px;
border-radius: 50%;
background: #3b82f6;
animation: thinking-bounce 1.2s ease-in-out infinite;
}
i:nth-child(2) { animation-delay: 0.2s; }
i:nth-child(3) { animation-delay: 0.4s; }
}
@keyframes thinking-bounce {
0%, 80%, 100% { opacity: 0.25; transform: translateY(0); }
40% { opacity: 1; transform: translateY(-3px); }
}
/* 参考提示卡片 */
.reference-card {
display: block;
margin: 10px 0 0;
padding: 9px 10px;
border: 1px solid var(--ai-border);
border-radius: 10px;
color: var(--ai-text-2);
background: var(--ai-soft-2);
text-align: left;
cursor: pointer;
transition: border-color 0.16s ease, background 0.16s ease;
&:hover { border-color: var(--ai-accent); background: var(--ai-soft); }
}
.reference-card__title {
display: inline-flex;
align-items: center;
gap: 5px;
margin-bottom: 5px;
color: var(--ai-text);
font-size: var(--ai-fs-caption);
font-weight: var(--ai-fw-emphasized);
.el-icon { color: var(--ai-accent); font-size: var(--student-fs-body); }
}
.reference-card__text {
display: block;
color: var(--ai-text-2);
font-size: var(--ai-fs-caption);
line-height: var(--ai-lh-caption);
}
/* 一体化输入器 */
.composer {
position: relative;
margin-top: 10px;
border: 1px solid var(--ai-border-strong);
border-radius: 10px;
background: #ffffff;
transition: border-color 0.16s ease, box-shadow 0.16s ease;
&:focus-within {
border-color: var(--ai-accent);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
textarea {
display: block;
width: 100%;
/* 单行起步:正文 22 + 上下内边距 19 = 41,取 44;高度由 JS 按内容撑开 */
min-height: 44px;
/* 与 JS 上限一致:约 6 行,超出后框内滚动 */
max-height: 168px;
overflow-y: auto;
padding: 10px 38px 9px 11px;
border: 0;
outline: none;
resize: none;
color: var(--ai-text);
background: transparent;
font-family: inherit;
font-size: var(--ai-fs-body);
line-height: var(--ai-lh-body);
&::placeholder { color: var(--ai-gray-light); }
}
}
.composer__send {
position: absolute;
right: 6px;
bottom: 6px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: 0;
border-radius: 7px;
color: #ffffff;
background: var(--ai-accent);
cursor: pointer;
transition: background 0.16s ease;
.el-icon { font-size: var(--student-fs-h2); }
&:hover:not(:disabled) { background: var(--ai-accent-deep); }
&:disabled { cursor: not-allowed; opacity: 0.45; }
}
/* ===== 助评:分数行 + 明细 ===== */
.score-row {
display: flex;
align-items: baseline;
justify-content: space-between;
padding: 12px 2px 10px;
border-bottom: 1px solid var(--ai-border);
}
.score-row__label {
color: var(--ai-text-2);
font-size: var(--ai-fs-caption);
font-weight: var(--ai-fw-emphasized);
}
.score-row__value { display: inline-flex; align-items: baseline; gap: 3px; }
.score-row__value strong {
color: var(--ai-text);
font-size: var(--ai-fs-display);
font-weight: 700;
line-height: 1.1;
font-variant-numeric: tabular-nums;
}
.score-row__value em {
color: var(--ai-gray);
font-size: var(--ai-fs-caption);
font-style: normal;
}
.review-scroll {
flex: 1 1 auto;
min-height: 120px;
padding: 4px 2px;
overflow-y: auto;
}
.review-result { display: flex; flex-direction: column; }
.review-summary {
margin: 10px 0 0;
color: var(--ai-text);
font-size: var(--ai-fs-body);
font-weight: var(--ai-fw-emphasized);
line-height: var(--ai-lh-body);
}
/* 评分明细:分隔线列表,不铺底色 */
.criteria { display: flex; flex-direction: column; margin-top: 4px; }
.criterion {
padding: 8px 0;
border-bottom: 1px solid var(--ai-border);
}
.criterion__top {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
}
.criterion__name {
color: var(--ai-text);
font-size: var(--ai-fs-body);
font-weight: var(--ai-fw-emphasized);
}
.criterion__score {
flex: 0 0 auto;
color: var(--ai-text);
font-size: var(--ai-fs-body);
font-weight: var(--ai-fw-emphasized);
font-variant-numeric: tabular-nums;
em {
color: var(--ai-gray);
font-size: var(--ai-fs-caption);
font-style: normal;
}
}
.criterion__desc {
margin: 4px 0 0;
color: var(--ai-gray);
font-size: var(--ai-fs-caption);
line-height: var(--ai-lh-caption);
}
/* 优先改进:浅灰块 + 墨黑左条 */
.improvements {
margin-top: 10px;
padding: 9px 10px 9px 11px;
border-left: 3px solid var(--ai-accent);
border-radius: 0 8px 8px 0;
background: var(--ai-soft-2);
}
.improvements__title {
display: block;
color: var(--ai-text);
font-size: var(--ai-fs-body);
font-weight: var(--ai-fw-emphasized);
}
.improvements ul {
display: grid;
gap: 4px;
margin: 6px 0 0;
padding-left: 16px;
color: var(--ai-text-2);
font-size: var(--ai-fs-caption);
line-height: var(--ai-lh-caption);
}
/* 空状态 */
.review-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 180px;
color: var(--ai-gray);
.el-icon { color: var(--ai-gray-light); font-size: 22px; }
p {
margin: 0;
font-size: var(--ai-fs-caption);
line-height: var(--ai-lh-caption);
text-align: center;
}
}
.review-hint {
margin: 10px 0 0;
color: var(--ai-gray);
font-size: var(--ai-fs-caption);
line-height: var(--ai-lh-caption);
}
/* 助评主按钮:墨黑实底 */
.review-primary {
display: inline-flex;
align-items: center;
justify-content: center;
height: 40px;
margin-top: 10px;
border: 1px solid var(--ai-accent);
border-radius: 10px;
color: #ffffff;
background: var(--ai-accent);
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.22);
font-family: inherit;
font-size: var(--ai-fs-body);
font-weight: var(--ai-fw-emphasized);
cursor: pointer;
transition: background 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease;
&:hover:not(:disabled) {
border-color: var(--ai-accent-deep);
background: var(--ai-accent-deep);
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.28);
}
&:disabled { cursor: not-allowed; opacity: 0.5; box-shadow: none; }
}
</style>