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
25 KiB
Vue

<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: #1f2937;
--ai-accent-deep: #111827;
--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(390px, 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(31, 41, 55, 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(31, 41, 55, 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: 15px; }
}
.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: 13px; }
&:hover { color: var(--ai-text); background: var(--ai-soft); }
&.on {
color: #ffffff;
background: var(--ai-accent);
box-shadow: 0 2px 6px rgba(31, 41, 55, 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: 13px; }
}
.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(31, 41, 55, 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: 14px; }
&: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(31, 41, 55, 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(31, 41, 55, 0.28);
}
&:disabled { cursor: not-allowed; opacity: 0.5; box-shadow: none; }
}
</style>