题目页重构之后按「功能对照 / 状态 / 结果页 / 布局与后端」四路审查了一遍,查出的问题一起修: 提交与换题 - 提交、画流程图途中换题:A 题格式化好的代码会盖掉 B 的草稿,A 判对了把 B 标成已解决、 弹 B 的点评、题单里跳回题单。现在每个请求回来先核对题目,轮询和推送回来的也核对提交编号 - 画着流程图换到不能画的题,语言退到题目的第一门、还被记成偏好;现在回到学生习惯的那门 - 强制点评评完点「继续 / 下一题」,学生之前在看「题目」页签的话会停在原题 - 离开题目页之后判题、流程图的 WebSocket 一直连着,现在闲 15 分钟断开 运行例子 - 输出开头的空格被削掉,打印菱形这类题对的代码也显示「没对上」 - C 的编译警告拼进了「你的输出」(后台生成测试点也会写进 .out) - Python 的语法错误(中文标点)被说成「下标有没有越界、除数是不是 0」 - 比赛里仍然把空格画成「·」、把不同的地方标红 入口与协作 - 比赛里不给「课堂统计」、不绑协作:房间和统计都按题号找,比赛题号和公开题撞号 - 「看他交过什么」按整名匹配,不再混进学号相近的同学 - 协作中点上下文条、相似题、下一题开新标签,协作不断 - 题单入口给管理员的「⋯」里留「本题提交」 结果与题面 - 「去自己输入拿 51 这些数试试」取到了题面 HTML 里的颜色值;「题目原话」露出 " - 流程图 A 级的学生标题下还写着「还没交过」 - 「我的提交」上方补回「答案错误 × 3」这样的计数 - 流程图历次评分能点开看那一版的图;读分数失败不再说「还没交过流程图」;手机上能看流程图点评 - 「结果」页签有新结果没看时加粗上色,看过就恢复 - 空画布拖进第一个节点画布会跳一下;画布底下空着一条 - 比赛没开始写成「已结束」、倒计时超过一个月算错;分隔条拖到头工具栏被挤坏;题单里的题目页有备案页脚 - 流程图历次分数只读需要的几列(一个学生一道题 23 次提交:96 kB → 2 kB) - 比赛密码、比赛没开始等几条学生看得到的英文报错换成中文 设计文档照实际做法更新了第 5.5、5.6、6、8、9、10、11 节。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
617 lines
17 KiB
Vue
617 lines
17 KiB
Vue
<script setup lang="ts">
|
||
import { useThemeVars } from "naive-ui"
|
||
import { storeToRefs } from "pinia"
|
||
import { useCodeStore } from "oj/store/code"
|
||
import { isFlowchartPass, useFlowchartStore } from "oj/store/flowchart"
|
||
import { useProblemStore } from "oj/store/problem"
|
||
import { useUserStore } from "shared/store/user"
|
||
import { sortFlowchartCriteria } from "utils/constants"
|
||
import { decompressFromBase64, parseTime } from "utils/functions"
|
||
|
||
/**
|
||
* 「结果」页签里流程图作业的那一块:AI 在评 / 评完了几分 / 历次分数 / 评语和改进建议。
|
||
*
|
||
* 原来评分详情是提交按钮旁一个 1000px 的弹框:盖住画布,学生没法一边看改进建议一边改图;
|
||
* 翻历次评分要一页一页点分页器。现在摆在左栏,画布整列都在右边(设计文档第 6 节)。
|
||
*
|
||
* viewOnly:手机上只能看、不能画(设计文档 5.6),不给「照着它写代码」「载回画布」。
|
||
*/
|
||
const { viewOnly = false } = defineProps<{ viewOnly?: boolean }>()
|
||
|
||
const MyFlowchart = defineAsyncComponent(() => import("./MyFlowchart.vue"))
|
||
|
||
const flowchartStore = useFlowchartStore()
|
||
const { phase, scores, hiddenCount, selectedId, details, detailLoading, scoresFailed, editor } =
|
||
storeToRefs(flowchartStore)
|
||
const problemStore = useProblemStore()
|
||
const codeStore = useCodeStore()
|
||
const userStore = useUserStore()
|
||
|
||
// 历次分数原来只由题面(ProblemContent)去读。手机上题面是另一个页签、不切过去就不挂载,
|
||
// 直接打开 ?tab=result 的话「流程图点评」一条都没有。这里自己也读一次(同一道题只读一次)
|
||
watch(
|
||
() => [problemStore.problem?.id, userStore.isAuthed] as const,
|
||
([id, authed]) => {
|
||
if (id && authed) flowchartStore.ensureLoaded()
|
||
},
|
||
{ immediate: true },
|
||
)
|
||
const message = useMessage()
|
||
const theme = useThemeVars()
|
||
// 暗色主题的成功色是浅薄荷绿,上面的白字看不清
|
||
const isDark = useDark()
|
||
const passText = computed(() => (isDark.value ? "rgba(0, 0, 0, 0.85)" : "#fff"))
|
||
|
||
/** 正在看的那一次:点过哪次就是哪次,否则是最新的 */
|
||
const current = computed(() => {
|
||
const id = selectedId.value ?? scores.value.at(-1)?.id
|
||
return id ? (scores.value.find((row) => row.id === id) ?? null) : null
|
||
})
|
||
const detail = computed(() => (current.value ? details.value[current.value.id] : undefined))
|
||
const viewingLatest = computed(() => selectedId.value === null)
|
||
|
||
// ==================== 这一版的图 ====================
|
||
/**
|
||
* 点历史分数条上的哪一次,就摆哪一次交上去的图(只读的 Mermaid 渲染,不动画布)。
|
||
* 原来的评分弹框左边就是这张图;挪进结果页签时丢了,学生翻到「第 2 次 C 42」
|
||
* 只看得到评语,想不起来那一版画成什么样。图从评语同一个详情接口来,随评语一起现拉。
|
||
*/
|
||
const versionIndex = computed(() =>
|
||
current.value ? scores.value.findIndex((row) => row.id === current.value!.id) : -1,
|
||
)
|
||
const versionTitle = computed(() => {
|
||
if (!current.value) return ""
|
||
return `第 ${versionIndex.value + 1} 次画的图 · ${parseTime(current.value.createTime, "MM-DD HH:mm")}`
|
||
})
|
||
/** 收起来之后翻别的次也保持收着:左栏在机房屏上只有六百来像素高 */
|
||
const versionOpen = ref(true)
|
||
const versionZoom = ref(false)
|
||
|
||
// 评完的那次、点到的那次,评语都要现拉(历次分数里只有分数)
|
||
watch(
|
||
() => current.value?.id,
|
||
(id) => {
|
||
if (id && !details.value[id]) flowchartStore.select(selectedId.value)
|
||
},
|
||
{ immediate: true },
|
||
)
|
||
|
||
const criteria = computed(() =>
|
||
sortFlowchartCriteria(
|
||
(detail.value?.aiCriteriaDetails ?? {}) as Record<
|
||
string,
|
||
{ score: number; max: number; comment?: string }
|
||
>,
|
||
),
|
||
)
|
||
const suggestions = computed(() =>
|
||
(detail.value?.aiSuggestions ?? "")
|
||
.split("\n")
|
||
.map((line) => line.trim())
|
||
.filter(Boolean),
|
||
)
|
||
|
||
function gradeType(grade: string) {
|
||
if (isFlowchartPass(grade)) return "success"
|
||
if (grade === "B") return "warning"
|
||
return "error"
|
||
}
|
||
|
||
function percentType(ratio: number) {
|
||
if (ratio >= 0.6) return "success"
|
||
if (ratio >= 0.4) return "warning"
|
||
return "error"
|
||
}
|
||
|
||
// ==================== 载回画布 ====================
|
||
/** 这一版的画布数据(压缩前的 JSON 字符串);老提交的压缩数据可能是坏的 */
|
||
const versionJson = computed(() => {
|
||
const data = detail.value?.flowchartData?.data
|
||
if (typeof data !== "string" || !data) return null
|
||
try {
|
||
return decompressFromBase64(data)
|
||
} catch {
|
||
return null
|
||
}
|
||
})
|
||
|
||
/**
|
||
* 一张图「长什么样」:节点的 id / 类型 / data(字写在里面)/ 位置,连线的两端和标签。不直接比 JSON 字符串 ——
|
||
* 载回画布时编辑器会补上默认字段(sourceHandle: null 之类),老提交还带着 vue-flow 的运行时
|
||
* 字段,字符串永远对不上,按钮就一直在。
|
||
*/
|
||
type Shape = { nodes?: unknown[]; edges?: unknown[] }
|
||
function signature(data: Shape) {
|
||
const nodes = (data.nodes ?? []).map((raw) => {
|
||
const node = raw as {
|
||
id?: string
|
||
type?: string
|
||
data?: unknown
|
||
position?: { x?: number; y?: number }
|
||
}
|
||
return [
|
||
node.id,
|
||
node.type,
|
||
// 节点上的字(customLabel)和原始类型都在 data 里,整个带上
|
||
JSON.stringify(node.data ?? {}),
|
||
Math.round(node.position?.x ?? 0),
|
||
Math.round(node.position?.y ?? 0),
|
||
].join("|")
|
||
})
|
||
const edges = (data.edges ?? []).map((raw) => {
|
||
const edge = raw as {
|
||
source?: string
|
||
target?: string
|
||
sourceHandle?: string | null
|
||
targetHandle?: string | null
|
||
label?: unknown
|
||
}
|
||
return [
|
||
edge.source,
|
||
edge.target,
|
||
edge.sourceHandle ?? "",
|
||
edge.targetHandle ?? "",
|
||
edge.label ?? "",
|
||
].join("|")
|
||
})
|
||
return JSON.stringify([nodes.sort(), edges.sort()])
|
||
}
|
||
|
||
const versionSignature = computed(() => {
|
||
if (!versionJson.value) return null
|
||
try {
|
||
return signature(JSON.parse(versionJson.value) as Shape)
|
||
} catch {
|
||
return null
|
||
}
|
||
})
|
||
|
||
/**
|
||
* 只有画布和这一版不一样时才给「载回画布」:刚交完、还没动过的时候,这个按钮点了什么
|
||
* 都不会变,只会让人以为它坏了。读 getFlowchartData() 会顺带跟踪画布的节点和边。
|
||
*/
|
||
const canvasDiffers = computed(() => {
|
||
if (!editor.value || !versionSignature.value) return false
|
||
return signature(editor.value.getFlowchartData()) !== versionSignature.value
|
||
})
|
||
|
||
function loadToCanvas() {
|
||
if (!editor.value || !versionJson.value) return
|
||
try {
|
||
const json = JSON.parse(versionJson.value)
|
||
editor.value.setFlowchartData({ nodes: json.nodes || [], edges: json.edges || [] })
|
||
message.success("已载回画布,按 Ctrl+Z 可以撤回")
|
||
} catch {
|
||
message.error("这一版的图数据坏了,载不回画布")
|
||
}
|
||
}
|
||
|
||
/** A / S 之后:照着图写代码。切回学生习惯的那门语言 */
|
||
const canCode = computed(() => problemStore.codeLanguages.length > 0)
|
||
function goCode() {
|
||
problemStore.switchLanguage(problemStore.supportedLanguage(codeStore.preferredLanguage()))
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<!-- 设计稿「画流程图:画布占满右栏,AI 点评在左边」「流程图评分中」 -->
|
||
<div class="flowchart-result">
|
||
<div v-if="scoresFailed && !scores.length && phase !== 'evaluating'" class="failed load-failed">
|
||
<span>以前交过的流程图评分没读出来,可能是网络断了一下</span>
|
||
<n-button size="small" secondary @click="flowchartStore.ensureLoaded()">再读一次</n-button>
|
||
</div>
|
||
<n-empty
|
||
v-else-if="!scores.length && phase !== 'evaluating'"
|
||
class="empty"
|
||
:description="
|
||
viewOnly
|
||
? '还没交过流程图。流程图要在电脑上画(手机上拖不动),交给 AI 点评之后,评分和建议在这里也看得到'
|
||
: '还没交过流程图。画好之后点「交给 AI 点评」,评分和建议会出现在这里'
|
||
"
|
||
/>
|
||
|
||
<!-- 历次分数连成一条:一眼看出是不是越改越好;正在评的那次是虚线框 -->
|
||
<div v-if="scores.length || phase === 'evaluating'" class="history">
|
||
<span class="history-lead"
|
||
>AI 点评 · 画过 {{ scores.length + (phase === "evaluating" ? 1 : 0) }} 次:</span
|
||
>
|
||
<template v-for="(row, index) in scores" :key="row.id">
|
||
<span v-if="index > 0" class="arrow" aria-hidden="true">→</span>
|
||
<button
|
||
type="button"
|
||
class="attempt"
|
||
:class="{
|
||
pass: isFlowchartPass(row.grade),
|
||
active: current?.id === row.id && !(phase === 'evaluating' && viewingLatest),
|
||
}"
|
||
:title="`第 ${index + 1} 次 · ${parseTime(row.createTime, 'MM-DD HH:mm')}`"
|
||
@click="flowchartStore.select(index === scores.length - 1 ? null : row.id)"
|
||
>
|
||
{{ row.grade }} {{ row.score }}
|
||
</button>
|
||
</template>
|
||
<template v-if="phase === 'evaluating'">
|
||
<span v-if="scores.length" class="arrow" aria-hidden="true">→</span>
|
||
<span class="attempt pending">第 {{ scores.length + 1 }} 次</span>
|
||
</template>
|
||
</div>
|
||
<p v-if="hiddenCount" class="note">
|
||
另外 {{ hiddenCount }} 次是加入题单之前画的,先藏起来了:在题单里画到 A 或 S 就解锁
|
||
</p>
|
||
|
||
<!-- 正在评:评的是最新这一次,所以只在看最新时显示 -->
|
||
<template v-if="phase === 'evaluating' && viewingLatest">
|
||
<div class="grading">
|
||
<n-spin :size="26" />
|
||
<div>
|
||
<div class="grading-title">AI 正在看你的图</div>
|
||
<div class="grading-note">一般 5 秒左右。可以先去看题目,评完这里会亮起来</div>
|
||
</div>
|
||
</div>
|
||
<p class="note">评的时候画布上还能改,改了不影响这一次的分数</p>
|
||
</template>
|
||
|
||
<div v-else-if="phase === 'failed' && viewingLatest" class="failed">
|
||
AI 这次没评出来,再点一次「交给 AI 点评」试试
|
||
</div>
|
||
|
||
<section v-else-if="current" class="current">
|
||
<div class="score-line">
|
||
<span class="score" :class="gradeType(current.grade)">{{ current.score }}</span>
|
||
<span class="score-unit">分</span>
|
||
<span class="grade" :class="gradeType(current.grade)">{{ current.grade }} 级</span>
|
||
<span v-if="detail?.aiFeedback || isFlowchartPass(current.grade)" class="feedback">
|
||
{{ detail?.aiFeedback ?? ""
|
||
}}{{ isFlowchartPass(current.grade) ? " 这道题算做完了。" : "" }}
|
||
</span>
|
||
</div>
|
||
|
||
<n-flex v-if="detailLoading && !detail" justify="center" class="pad">
|
||
<n-spin size="small" />
|
||
</n-flex>
|
||
|
||
<template v-if="detail">
|
||
<div v-if="detail.mermaidCode" class="version">
|
||
<div class="version-head">
|
||
<span class="version-title">{{ versionTitle }}</span>
|
||
<n-button text type="primary" size="small" @click="versionZoom = true">
|
||
放大看
|
||
</n-button>
|
||
<n-button text size="small" @click="versionOpen = !versionOpen">
|
||
{{ versionOpen ? "收起" : "展开" }}
|
||
</n-button>
|
||
</div>
|
||
<div
|
||
v-if="versionOpen"
|
||
class="version-chart"
|
||
role="button"
|
||
:aria-label="`放大看${versionTitle}`"
|
||
@click="versionZoom = true"
|
||
>
|
||
<MyFlowchart compact :code="detail.mermaidCode" />
|
||
</div>
|
||
<n-modal
|
||
v-model:show="versionZoom"
|
||
preset="card"
|
||
:title="versionTitle"
|
||
:style="{ maxWidth: '900px' }"
|
||
>
|
||
<MyFlowchart :code="detail.mermaidCode" />
|
||
</n-modal>
|
||
</div>
|
||
|
||
<div v-if="criteria.length" class="criteria">
|
||
<div v-for="[name, item] in criteria" :key="name" class="criterion">
|
||
<div class="criterion-head">
|
||
<b>{{ name }}</b>
|
||
<span class="criterion-score">{{ item.score || 0 }} / {{ item.max }}</span>
|
||
</div>
|
||
<div class="bar">
|
||
<div
|
||
class="bar-fill"
|
||
:class="percentType(item.score / item.max)"
|
||
:style="{ width: `${Math.min(100, ((item.score || 0) / item.max) * 100)}%` }"
|
||
/>
|
||
</div>
|
||
<div v-if="item.comment" class="comment">{{ item.comment }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="suggestions.length" class="suggestions">
|
||
<div class="suggestions-title">还可以改进</div>
|
||
<div v-for="(line, index) in suggestions" :key="index" class="suggestion">
|
||
<span v-if="index === 0" class="tag">重点</span>
|
||
<span>{{ line }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="!viewOnly" class="actions">
|
||
<n-button
|
||
v-if="isFlowchartPass(current.grade) && canCode"
|
||
type="primary"
|
||
size="large"
|
||
@click="goCode"
|
||
>
|
||
照着它写代码 →
|
||
</n-button>
|
||
<n-button v-if="canvasDiffers" secondary @click="loadToCanvas">把这一版载回画布</n-button>
|
||
</div>
|
||
</template>
|
||
</section>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.flowchart-result {
|
||
padding-bottom: 16px;
|
||
}
|
||
|
||
.empty {
|
||
margin-top: 48px;
|
||
}
|
||
|
||
.note {
|
||
font-size: 13px;
|
||
color: v-bind("theme.textColor3");
|
||
margin: 0 0 10px;
|
||
}
|
||
|
||
.pad {
|
||
padding: 16px 0;
|
||
}
|
||
|
||
.history {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
margin-bottom: 12px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.history-lead {
|
||
color: v-bind("theme.textColor2");
|
||
}
|
||
|
||
.arrow {
|
||
color: v-bind("theme.textColor3");
|
||
}
|
||
|
||
.attempt {
|
||
height: 26px;
|
||
box-sizing: border-box;
|
||
padding: 0 9px;
|
||
border-radius: 13px;
|
||
border: 1px solid transparent;
|
||
background-color: rgba(128, 128, 128, 0.12);
|
||
color: v-bind("theme.textColor2");
|
||
font: inherit;
|
||
font-size: 13px;
|
||
font-variant-numeric: tabular-nums;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.attempt.active {
|
||
background-color: v-bind("theme.textColor2");
|
||
color: v-bind("theme.cardColor");
|
||
font-weight: 600;
|
||
}
|
||
|
||
.attempt.pass.active {
|
||
background-color: v-bind("theme.successColor");
|
||
color: v-bind("passText");
|
||
}
|
||
|
||
.attempt.pending {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
cursor: default;
|
||
background: transparent;
|
||
border: 1px dashed v-bind("theme.successColor");
|
||
color: v-bind("theme.successColorPressed");
|
||
}
|
||
|
||
.grading {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
padding: 18px;
|
||
margin-bottom: 10px;
|
||
border-radius: 8px;
|
||
background-color: rgba(24, 160, 88, 0.07);
|
||
border: 1px solid rgba(24, 160, 88, 0.25);
|
||
}
|
||
|
||
.grading-title {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.grading-note {
|
||
margin-top: 2px;
|
||
font-size: 13px;
|
||
color: v-bind("theme.textColor2");
|
||
}
|
||
|
||
.failed {
|
||
padding: 12px 14px;
|
||
border-radius: 6px;
|
||
background-color: rgba(240, 160, 32, 0.1);
|
||
border: 1px solid rgba(240, 160, 32, 0.35);
|
||
}
|
||
|
||
.load-failed {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 8px 12px;
|
||
}
|
||
|
||
/* 这一版的图:和题面里「你画的流程图」同一个样子,点一下放大 */
|
||
.version {
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.version-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.version-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.version-chart {
|
||
cursor: zoom-in;
|
||
border-radius: 6px;
|
||
background-color: rgba(128, 128, 128, 0.06);
|
||
padding: 8px;
|
||
}
|
||
|
||
.version-chart :deep(.flowchart-container.compact) {
|
||
height: 180px;
|
||
}
|
||
|
||
.score-line {
|
||
display: flex;
|
||
align-items: baseline;
|
||
flex-wrap: wrap;
|
||
gap: 4px 8px;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.score {
|
||
font-size: 34px;
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.score.success {
|
||
color: v-bind("theme.successColorPressed");
|
||
}
|
||
|
||
.score.warning {
|
||
color: v-bind("theme.warningColorPressed");
|
||
}
|
||
|
||
.score.error {
|
||
color: v-bind("theme.errorColorPressed");
|
||
}
|
||
|
||
.score-unit {
|
||
color: v-bind("theme.textColor2");
|
||
}
|
||
|
||
.grade {
|
||
align-self: center;
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
color: #fff;
|
||
background-color: v-bind("theme.errorColor");
|
||
}
|
||
|
||
.grade.success {
|
||
background-color: v-bind("theme.successColor");
|
||
color: v-bind("passText");
|
||
}
|
||
|
||
.grade.warning {
|
||
background-color: v-bind("theme.warningColor");
|
||
}
|
||
|
||
.feedback {
|
||
flex: 1 1 200px;
|
||
align-self: center;
|
||
margin-left: 6px;
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
/* 四项得分:两列,每项一条进度 */
|
||
.criteria {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 12px 20px;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.criterion-head {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.criterion-score {
|
||
font-variant-numeric: tabular-nums;
|
||
color: v-bind("theme.textColor2");
|
||
}
|
||
|
||
.bar {
|
||
height: 6px;
|
||
margin: 4px 0;
|
||
border-radius: 3px;
|
||
background-color: rgba(128, 128, 128, 0.14);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.bar-fill {
|
||
height: 100%;
|
||
border-radius: 3px;
|
||
background-color: v-bind("theme.successColor");
|
||
}
|
||
|
||
.bar-fill.warning {
|
||
background-color: v-bind("theme.warningColor");
|
||
}
|
||
|
||
.bar-fill.error {
|
||
background-color: v-bind("theme.errorColor");
|
||
}
|
||
|
||
.comment {
|
||
font-size: 13px;
|
||
color: v-bind("theme.textColor2");
|
||
}
|
||
|
||
.suggestions {
|
||
padding: 12px 14px;
|
||
margin-bottom: 14px;
|
||
border-radius: 6px;
|
||
background-color: rgba(128, 128, 128, 0.06);
|
||
border: 1px solid v-bind("theme.dividerColor");
|
||
}
|
||
|
||
.suggestions-title {
|
||
font-weight: 600;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.suggestion {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 8px;
|
||
line-height: 1.7;
|
||
}
|
||
|
||
.tag {
|
||
flex: none;
|
||
padding: 0 6px;
|
||
border-radius: 3px;
|
||
font-size: 12px;
|
||
background-color: rgba(240, 160, 32, 0.16);
|
||
color: v-bind("theme.warningColorPressed");
|
||
}
|
||
|
||
.actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
</style>
|