AiQAMarkdown.vue 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878
  1. <template>
  2. <div ref="contentRef" class="markdown-view" :class="`markdown-view--${props.theme}`" @click="handleCitationClick">
  3. <template v-for="segment in renderedSegments" :key="segment.key">
  4. <div v-if="segment.type === 'markdown'" class="markdown-fragment" v-html="segment.html"></div>
  5. <section v-else class="markdown-score-radar" :data-score-block-key="segment.blockKey">
  6. <div class="markdown-score-radar__header">
  7. <div>
  8. <strong>{{ segment.data.studentName }} · AI评分雷达图</strong>
  9. <small>满分 {{ segment.data.maxScore }} 分</small>
  10. </div>
  11. <button class="markdown-score-radar__action" type="button" data-copy-exclude @click.stop="openExpertDialog(segment)">
  12. {{ hasExpertScores(segment) ? '修改专家评分' : '补充专家评分' }}
  13. </button>
  14. </div>
  15. <svg class="markdown-score-radar__chart" viewBox="0 0 420 350" role="img" :aria-label="`${segment.data.studentName}评分雷达图`">
  16. <g transform="translate(210 164)">
  17. <polygon
  18. v-for="level in segment.data.maxScore"
  19. :key="`grid-${level}`"
  20. class="markdown-score-radar__grid"
  21. :points="gridPoints(segment.data.dimensions.length, level / segment.data.maxScore)"
  22. />
  23. <line
  24. v-for="(_, index) in segment.data.dimensions"
  25. :key="`axis-${index}`"
  26. class="markdown-score-radar__axis"
  27. x1="0"
  28. y1="0"
  29. :x2="polarPoint(index, segment.data.dimensions.length, 1).x"
  30. :y2="polarPoint(index, segment.data.dimensions.length, 1).y"
  31. />
  32. <polygon class="markdown-score-radar__area markdown-score-radar__area--ai" :points="scorePoints(segment, 'ai')" />
  33. <polygon v-if="hasExpertScores(segment)" class="markdown-score-radar__area markdown-score-radar__area--expert" :points="scorePoints(segment, 'expert')" />
  34. <template v-for="(dimension, index) in segment.data.dimensions" :key="`point-${dimension.key}`">
  35. <circle
  36. v-if="dimension.aiScore !== null"
  37. class="markdown-score-radar__point markdown-score-radar__point--ai"
  38. :cx="scorePoint(segment, index, 'ai').x"
  39. :cy="scorePoint(segment, index, 'ai').y"
  40. r="3.5"
  41. />
  42. <circle
  43. v-if="expertScore(segment, dimension.key) !== null"
  44. class="markdown-score-radar__point markdown-score-radar__point--expert"
  45. :cx="scorePoint(segment, index, 'expert').x"
  46. :cy="scorePoint(segment, index, 'expert').y"
  47. r="3.5"
  48. />
  49. <text
  50. class="markdown-score-radar__label"
  51. :x="labelPoint(index, segment.data.dimensions.length).x"
  52. :y="labelPoint(index, segment.data.dimensions.length).y"
  53. :text-anchor="labelAnchor(index, segment.data.dimensions.length)"
  54. dominant-baseline="middle"
  55. >{{ dimension.shortName }}</text>
  56. </template>
  57. </g>
  58. </svg>
  59. <div class="markdown-score-radar__legend">
  60. <span><i class="markdown-score-radar__legend-dot markdown-score-radar__legend-dot--ai"></i>AI评分</span>
  61. <span v-if="hasExpertScores(segment)"><i class="markdown-score-radar__legend-dot markdown-score-radar__legend-dot--expert"></i>专家评分</span>
  62. </div>
  63. <div class="markdown-score-radar__score-list">
  64. <div v-for="dimension in segment.data.dimensions" :key="dimension.key">
  65. <span>{{ dimension.name }}</span>
  66. <b>AI:{{ formatScore(dimension.aiScore) }}</b>
  67. <b v-if="hasExpertScores(segment)">专家:{{ formatScore(expertScore(segment, dimension.key)) }}</b>
  68. </div>
  69. </div>
  70. </section>
  71. </template>
  72. </div>
  73. <el-dialog v-model="expertDialogVisible" title="补充专家评分" width="min(520px, 92vw)" append-to-body destroy-on-close>
  74. <p class="markdown-score-dialog__hint">{{ editingSegment?.data.studentName }} · 评分范围1–5分,可留空表示未评分</p>
  75. <div class="markdown-score-dialog__list">
  76. <label v-for="dimension in editingSegment?.data.dimensions || []" :key="dimension.key">
  77. <span>{{ dimension.name }}</span>
  78. <el-select v-model="expertDraft[dimension.key]" clearable placeholder="未评分">
  79. <el-option v-for="score in 5" :key="score" :label="`${score}分`" :value="score" />
  80. </el-select>
  81. </label>
  82. </div>
  83. <template #footer>
  84. <el-button @click="expertDialogVisible = false">取消</el-button>
  85. <el-button type="primary" @click="saveExpertScores">确认并生成对比</el-button>
  86. </template>
  87. </el-dialog>
  88. </template>
  89. <script setup lang="ts">
  90. import MarkdownIt from 'markdown-it'
  91. import 'highlight.js/styles/vs2015.min.css'
  92. import hljs from 'highlight.js'
  93. import { computed, reactive, ref } from 'vue'
  94. const props = defineProps({
  95. content: {
  96. type: String,
  97. required: true
  98. },
  99. citationIds: {
  100. type: Array,
  101. default: () => []
  102. },
  103. theme: {
  104. type: String,
  105. default: 'light',
  106. validator: (value: string) => ['light', 'dark'].includes(value)
  107. },
  108. enableStudentScore: {
  109. type: Boolean,
  110. default: false
  111. },
  112. scoreOverrides: {
  113. type: Object,
  114. default: () => ({})
  115. }
  116. })
  117. const emit = defineEmits(['citation-click', 'student-score-change'])
  118. const contentRef = ref()
  119. const expertDialogVisible = ref(false)
  120. const editingSegment = ref<any>(null)
  121. const expertDraft = reactive<Record<string, number | null>>({})
  122. const SCORE_DIMENSION_KEYS = [
  123. 'customer_context',
  124. 'customer_insight',
  125. 'policy_interpretation',
  126. 'opportunity_risk',
  127. 'objection_reframing',
  128. 'fabetc'
  129. ]
  130. type ScoreDimension = {
  131. key: string
  132. name: string
  133. shortName: string
  134. aiScore: number | null
  135. }
  136. type StudentScoreData = {
  137. studentId: string
  138. studentName: string
  139. maxScore: number
  140. dimensions: ScoreDimension[]
  141. }
  142. type MarkdownSegment = {
  143. key: string
  144. type: 'markdown'
  145. html: string
  146. }
  147. type RadarSegment = {
  148. key: string
  149. type: 'radar'
  150. blockKey: string
  151. data: StudentScoreData
  152. }
  153. type RenderedSegment = MarkdownSegment | RadarSegment
  154. /**
  155. * 将表格行按 | 分割,去除首尾空字符串,返回单元格数组
  156. */
  157. function parseTableRow(line: string): string[] {
  158. const parts = line.split('|')
  159. // 去除首尾空元素(因为 | 在开头和结尾)
  160. if (parts[0]?.trim() === '') parts.shift()
  161. if (parts[parts.length - 1]?.trim() === '') parts.pop()
  162. return parts.map(p => p.trim())
  163. }
  164. /**
  165. * 检查字符串是否为有效的表格分隔符单元格
  166. * 标准格式:---, :---, ---:, :---:
  167. */
  168. function isSeparatorCell(cell: string): boolean {
  169. return /^:?-{2,}:?$/.test(cell)
  170. }
  171. /**
  172. * 预处理 Markdown 内容,修复不规范的表格格式
  173. * 主要修复:分隔符行列数不匹配、格式错误、多余的竖线、空单元格格式问题
  174. */
  175. function normalizeMarkdownTables(content: string): string {
  176. if (!content) return content
  177. const lines = content.split('\n')
  178. const result: string[] = []
  179. let i = 0
  180. while (i < lines.length) {
  181. const line = lines[i]
  182. const trimmedLine = line.trim()
  183. // 检测可能的表格起始行(以 | 开头结尾,且包含多个单元格)
  184. if (trimmedLine.startsWith('|') && trimmedLine.endsWith('|') && trimmedLine.includes('|', 1) && i + 1 < lines.length) {
  185. const headerCells = parseTableRow(trimmedLine)
  186. const expectedCols = headerCells.length
  187. // 至少需要2列才能构成表格
  188. if (expectedCols >= 2) {
  189. const nextLineRaw = lines[i + 1]
  190. const nextLine = nextLineRaw?.trim() || ''
  191. // 检查下一行是否看起来像分隔符行(包含大量 | 和 -)
  192. const looksLikeSeparator = nextLine.startsWith('|') && nextLine.endsWith('|')
  193. && nextLine.includes('-') && /^\|[\s\-:|]+\|$/.test(nextLine)
  194. // 或尝试宽松检测:包含足够多的 | 和 - 字符
  195. const pipeCount = (nextLine.match(/\|/g) || []).length
  196. const dashCount = (nextLine.match(/-/g) || []).length
  197. const looseSeparatorMatch = pipeCount >= 2 && dashCount >= 3
  198. if (looksLikeSeparator || looseSeparatorMatch) {
  199. // 尝试解析分隔符行的单元格
  200. let sepCells = parseTableRow(nextLine)
  201. // 如果分隔符单元格解析后大部分是有效的分隔符格式,确认是表格
  202. const validSepCount = sepCells.filter(c => isSeparatorCell(c) || /^-+$/.test(c.replace(/\s/g, ''))).length
  203. if (validSepCount >= 1 || (pipeCount >= expectedCols - 1 && dashCount >= expectedCols * 2)) {
  204. // 确认是表格,开始修复
  205. // 修复分隔符行:确保列数与表头一致,每个单元格是 --- 格式
  206. const fixedSepCells: string[] = []
  207. for (let k = 0; k < expectedCols; k++) {
  208. const orig = sepCells[k] || ''
  209. // 保留原有的对齐标记(:)
  210. let leftAlign = orig.startsWith(':')
  211. let rightAlign = orig.endsWith(':')
  212. fixedSepCells.push((leftAlign ? ':' : '') + '---' + (rightAlign ? ':' : ''))
  213. }
  214. const fixedSepLine = '| ' + fixedSepCells.join(' | ') + ' |'
  215. const tableLines: string[] = []
  216. // 修复表头行,确保单元格之间格式正确
  217. tableLines.push('| ' + headerCells.join(' | ') + ' |')
  218. tableLines.push(fixedSepLine)
  219. let j = i + 2
  220. // 收集并修复数据行
  221. while (j < lines.length) {
  222. const dataLineRaw = lines[j]
  223. const dataLine = dataLineRaw?.trim() || ''
  224. if (dataLine.startsWith('|') && dataLine.endsWith('|')) {
  225. let dataCells = parseTableRow(dataLine)
  226. // 修复数据行列数:不足补空,多余截断
  227. if (dataCells.length < expectedCols) {
  228. while (dataCells.length < expectedCols) dataCells.push('')
  229. } else if (dataCells.length > expectedCols) {
  230. dataCells = dataCells.slice(0, expectedCols)
  231. }
  232. tableLines.push('| ' + dataCells.join(' | ') + ' |')
  233. j++
  234. } else if (dataLine === '') {
  235. // 空行可能表示表格结束,但也可能是表格内的换行(保守处理:表格结束)
  236. break
  237. } else {
  238. break
  239. }
  240. }
  241. result.push(...tableLines)
  242. i = j
  243. continue
  244. }
  245. }
  246. }
  247. }
  248. result.push(line)
  249. i++
  250. }
  251. return result.join('\n')
  252. }
  253. // ====== 创建 markdown-it 实例 ======
  254. const md = new MarkdownIt({
  255. html: true,
  256. linkify: true,
  257. typographer: true,
  258. breaks: false,
  259. highlight: function (str, lang) {
  260. if (lang && hljs.getLanguage(lang)) {
  261. try {
  262. const copyHtml = `<div id="copy" data-copy='${str}' style="position: absolute; right: 10px; top: 5px; color: #fff;cursor: pointer;">复制</div>`
  263. return `<pre style="position: relative;">${copyHtml}<code class="hljs">${hljs.highlight(lang, str, true).value}</code></pre>`
  264. } catch (__) {}
  265. }
  266. return `<pre><code>${escapeHtml(str)}</code></pre>`
  267. }
  268. })
  269. // ====== 关键修复:正确启用表格 ======
  270. md.enable('table')
  271. md.inline.ruler.before('text', 'citation_mark', (state, silent) => {
  272. const match = /^\[S(\d+)]/.exec(state.src.slice(state.pos, state.posMax))
  273. const citationIds = Array.isArray(state.env.citationIds) ? state.env.citationIds : []
  274. if (!match || !citationIds.includes(Number(match[1]))) return false
  275. if (silent) return true
  276. const token = state.push('citation_mark', '', 0)
  277. token.meta = { id: Number(match[1]) }
  278. token.content = match[0]
  279. state.pos += match[0].length
  280. return true
  281. })
  282. md.renderer.rules.citation_mark = (tokens, idx) => {
  283. const id = tokens[idx].meta.id
  284. return `<button class="citation-mark" type="button" data-citation-id="${id}">${tokens[idx].content}</button>`
  285. }
  286. function handleCitationClick(event: MouseEvent) {
  287. const target = event.target as HTMLElement | null
  288. const mark = target?.closest<HTMLElement>('[data-citation-id]')
  289. const segmentId = Number(mark?.dataset.citationId)
  290. if (Number.isFinite(segmentId)) emit('citation-click', segmentId)
  291. }
  292. function escapeHtml(text: string): string {
  293. const map: Record<string, string> = {
  294. '&': '&amp;',
  295. '<': '&lt;',
  296. '>': '&gt;',
  297. '"': '&quot;',
  298. "'": '&#039;'
  299. }
  300. return text.replace(/[&<>"']/g, function(m) { return map[m] })
  301. }
  302. // ====== 覆盖表格渲染规则 ======
  303. const defaultTableOpen = md.renderer.rules.table_open
  304. const defaultTableClose = md.renderer.rules.table_close
  305. md.renderer.rules.table_open = function(tokens, idx, options, env, self) {
  306. return '<div class="markdown-table-wrap"><table>'
  307. }
  308. md.renderer.rules.table_close = function(tokens, idx, options, env, self) {
  309. return '</table></div>'
  310. }
  311. // ====== 确保 tbody 正确渲染 ======
  312. md.renderer.rules.tbody_open = function(tokens, idx, options, env, self) {
  313. return '<tbody>'
  314. }
  315. md.renderer.rules.tbody_close = function(tokens, idx, options, env, self) {
  316. return '</tbody>'
  317. }
  318. function normalizeStudentScoreData(value: unknown): StudentScoreData | null {
  319. if (!value || typeof value !== 'object') return null
  320. const raw = value as Record<string, any>
  321. const maxScore = Number(raw.maxScore)
  322. if (!raw.studentName || !Number.isInteger(maxScore) || maxScore !== 5 || !Array.isArray(raw.dimensions)) return null
  323. if (raw.dimensions.length !== SCORE_DIMENSION_KEYS.length) return null
  324. const dimensions: ScoreDimension[] = []
  325. for (let index = 0; index < SCORE_DIMENSION_KEYS.length; index++) {
  326. const item = raw.dimensions[index]
  327. if (!item || item.key !== SCORE_DIMENSION_KEYS[index] || !item.name || !item.shortName) return null
  328. const aiScore = item.aiScore === null ? null : Number(item.aiScore)
  329. if (aiScore !== null && (!Number.isInteger(aiScore) || aiScore < 1 || aiScore > maxScore)) return null
  330. dimensions.push({
  331. key: String(item.key),
  332. name: String(item.name),
  333. shortName: String(item.shortName),
  334. aiScore
  335. })
  336. }
  337. return {
  338. studentId: String(raw.studentId || ''),
  339. studentName: String(raw.studentName),
  340. maxScore,
  341. dimensions
  342. }
  343. }
  344. function renderMarkdown(markdown: string): string {
  345. if (!markdown) return ''
  346. const normalizedContent = normalizeMarkdownTables(markdown)
  347. return md.render(normalizedContent, { citationIds: props.citationIds })
  348. }
  349. const renderedSegments = computed<any[]>(() => {
  350. if (!props.content) return []
  351. if (!props.enableStudentScore) {
  352. return [{ key: 'markdown-0', type: 'markdown', html: renderMarkdown(props.content) }]
  353. }
  354. const segments: RenderedSegment[] = []
  355. const pattern = /```student-score-json\s*\r?\n([\s\S]*?)\r?\n```/g
  356. let cursor = 0
  357. let blockIndex = 0
  358. let match: RegExpExecArray | null
  359. try {
  360. while ((match = pattern.exec(props.content)) !== null) {
  361. const before = props.content.slice(cursor, match.index)
  362. if (before) segments.push({ key: `markdown-${blockIndex}`, type: 'markdown', html: renderMarkdown(before) })
  363. let data: StudentScoreData | null = null
  364. try {
  365. data = normalizeStudentScoreData(JSON.parse(match[1]))
  366. } catch (_) {
  367. data = null
  368. }
  369. if (data) {
  370. const identity = data.studentId || data.studentName || `student-${blockIndex + 1}`
  371. const blockKey = `${identity}-${blockIndex}`
  372. segments.push({ key: `radar-${blockKey}`, type: 'radar', blockKey, data })
  373. } else {
  374. segments.push({ key: `markdown-invalid-${blockIndex}`, type: 'markdown', html: renderMarkdown(match[0]) })
  375. }
  376. cursor = pattern.lastIndex
  377. blockIndex += 1
  378. }
  379. const rest = props.content.slice(cursor)
  380. if (rest) segments.push({ key: `markdown-${blockIndex}`, type: 'markdown', html: renderMarkdown(rest) })
  381. return segments
  382. } catch (error) {
  383. console.error('Markdown 渲染失败:', error)
  384. return [{ key: 'markdown-error', type: 'markdown', html: escapeHtml(props.content) }]
  385. }
  386. })
  387. const RADAR_RADIUS = 104
  388. const LABEL_RADIUS = 132
  389. function polarPoint(index: number, count: number, ratio: number, radius = RADAR_RADIUS) {
  390. const angle = -Math.PI / 2 + (Math.PI * 2 * index) / count
  391. return {
  392. x: Number((Math.cos(angle) * radius * ratio).toFixed(2)),
  393. y: Number((Math.sin(angle) * radius * ratio).toFixed(2))
  394. }
  395. }
  396. function gridPoints(count: number, ratio: number) {
  397. return Array.from({ length: count }, (_, index) => {
  398. const point = polarPoint(index, count, ratio)
  399. return `${point.x},${point.y}`
  400. }).join(' ')
  401. }
  402. function getExpertScoreMap(segment: RadarSegment): Record<string, number | null> {
  403. const value = props.scoreOverrides?.[segment.blockKey]
  404. return value && typeof value === 'object' ? value : {}
  405. }
  406. function expertScore(segment: RadarSegment, key: string): number | null {
  407. const value = getExpertScoreMap(segment)[key]
  408. return Number.isInteger(value) && Number(value) >= 1 && Number(value) <= segment.data.maxScore ? Number(value) : null
  409. }
  410. function hasExpertScores(segment: RadarSegment) {
  411. return segment.data.dimensions.some((dimension) => expertScore(segment, dimension.key) !== null)
  412. }
  413. function scorePoint(segment: RadarSegment, index: number, source: 'ai' | 'expert') {
  414. const dimension = segment.data.dimensions[index]
  415. const score = source === 'ai' ? dimension.aiScore : expertScore(segment, dimension.key)
  416. return polarPoint(index, segment.data.dimensions.length, (score ?? 0) / segment.data.maxScore)
  417. }
  418. function scorePoints(segment: RadarSegment, source: 'ai' | 'expert') {
  419. return segment.data.dimensions.map((_, index) => {
  420. const point = scorePoint(segment, index, source)
  421. return `${point.x},${point.y}`
  422. }).join(' ')
  423. }
  424. function labelPoint(index: number, count: number) {
  425. return polarPoint(index, count, 1, LABEL_RADIUS)
  426. }
  427. function labelAnchor(index: number, count: number) {
  428. const x = labelPoint(index, count).x
  429. if (Math.abs(x) < 8) return 'middle'
  430. return x > 0 ? 'start' : 'end'
  431. }
  432. function formatScore(score: number | null) {
  433. return score === null ? '未评分' : `${score}分`
  434. }
  435. function openExpertDialog(segment: RadarSegment) {
  436. editingSegment.value = segment
  437. Object.keys(expertDraft).forEach((key) => delete expertDraft[key])
  438. segment.data.dimensions.forEach((dimension) => {
  439. expertDraft[dimension.key] = expertScore(segment, dimension.key)
  440. })
  441. expertDialogVisible.value = true
  442. }
  443. function saveExpertScores() {
  444. const segment = editingSegment.value as RadarSegment | null
  445. if (!segment) return
  446. const scores: Record<string, number | null> = {}
  447. segment.data.dimensions.forEach((dimension) => {
  448. const value = expertDraft[dimension.key]
  449. scores[dimension.key] = Number.isInteger(value) ? Number(value) : null
  450. })
  451. emit('student-score-change', {
  452. blockKey: segment.blockKey,
  453. studentId: segment.data.studentId,
  454. studentName: segment.data.studentName,
  455. scores
  456. })
  457. expertDialogVisible.value = false
  458. }
  459. </script>
  460. <style lang="scss">
  461. .markdown-view {
  462. --md-text: #000;
  463. --md-heading: #000;
  464. --md-table-wrap-bg: rgba(15, 23, 42, .02);
  465. --md-table-border: rgba(15, 23, 42, .18);
  466. --md-table-head-bg: #edf3fa;
  467. --md-table-head-text: #000;
  468. --md-table-cell-text: #000;
  469. --md-table-row-even: rgba(15, 23, 42, .035);
  470. --md-table-row-hover: rgba(74, 137, 231, .1);
  471. --md-scroll-track: rgba(15, 23, 42, .05);
  472. --md-scroll-thumb: rgba(74, 137, 231, .35);
  473. --md-scroll-thumb-hover: rgba(74, 137, 231, .55);
  474. font-family: 'SourceHanSansCN-Normal';
  475. font-weight: 400;
  476. letter-spacing: 0em;
  477. text-align: left;
  478. color: var(--md-text);
  479. max-width: 100%;
  480. &.markdown-view--dark {
  481. --md-text: #f0f5ff;
  482. --md-heading: #f0f5ff;
  483. --md-table-wrap-bg: rgba(255, 255, 255, .02);
  484. --md-table-border: rgba(174, 205, 255, .25);
  485. --md-table-head-bg: rgba(74, 137, 231, .4);
  486. --md-table-head-text: #fff;
  487. --md-table-cell-text: #e8f0ff;
  488. --md-table-row-even: rgba(255, 255, 255, .03);
  489. --md-table-row-hover: rgba(74, 137, 231, .1);
  490. --md-scroll-track: rgba(255, 255, 255, .04);
  491. --md-scroll-thumb: rgba(74, 137, 231, .35);
  492. --md-scroll-thumb-hover: rgba(74, 137, 231, .55);
  493. }
  494. pre {
  495. position: relative;
  496. }
  497. pre code.hljs {
  498. width: auto;
  499. }
  500. code.hljs {
  501. border-radius: 6px;
  502. padding-top: 20px;
  503. width: auto;
  504. @media screen and (min-width: 1536px) {
  505. width: 960px;
  506. }
  507. @media screen and (max-width: 1536px) and (min-width: 1024px) {
  508. width: calc(100vw - 400px - 64px - 32px * 2);
  509. }
  510. @media screen and (max-width: 1024px) and (min-width: 768px) {
  511. width: calc(100vw - 32px * 2);
  512. }
  513. @media screen and (max-width: 768px) {
  514. width: calc(100vw - 16px * 2);
  515. }
  516. }
  517. p, code.hljs {
  518. margin-bottom: 16px;
  519. }
  520. p {
  521. margin: 0;
  522. margin-bottom: 3px;
  523. }
  524. h1, h2, h3, h4, h5, h6 {
  525. color: var(--md-heading);
  526. margin: 24px 0 8px;
  527. font-weight: 600;
  528. }
  529. h1 { font-size: 22px; line-height: 32px; }
  530. h2 { font-size: 20px; line-height: 30px; }
  531. h3 { font-size: 18px; line-height: 28px; }
  532. h4 { font-size: 16px; line-height: 26px; }
  533. h5 { font-size: 16px; line-height: 24px; }
  534. h6 { font-size: 16px; line-height: 24px; }
  535. ul, ol {
  536. margin: 0 0 8px 0;
  537. padding: 0;
  538. font-size: 16px;
  539. line-height: 24px;
  540. color: var(--md-text);
  541. }
  542. li {
  543. margin: 4px 0 0 20px;
  544. margin-bottom: 1rem;
  545. }
  546. ol > li {
  547. list-style-type: decimal;
  548. margin-bottom: 1rem;
  549. }
  550. ul > li {
  551. list-style-type: disc;
  552. font-size: 16px;
  553. line-height: 24px;
  554. margin-right: 11px;
  555. margin-bottom: 1rem;
  556. color: var(--md-text);
  557. }
  558. ol ul, ol ul > li, ul ul, ul ul li {
  559. font-size: 16px;
  560. list-style: none;
  561. margin-left: 6px;
  562. margin-bottom: 1rem;
  563. }
  564. ul ul ul, ul ul ul li, ol ol, ol ol > li, ol ul ul, ol ul ul > li, ul ol, ul ol > li {
  565. list-style: square;
  566. }
  567. // 表格容器样式
  568. .markdown-table-wrap {
  569. display: block !important;
  570. overflow-x: auto;
  571. margin: 16px 0;
  572. border-radius: 8px;
  573. border: 1px solid var(--md-table-border) !important;
  574. -webkit-overflow-scrolling: touch;
  575. background: var(--md-table-wrap-bg);
  576. &::-webkit-scrollbar {
  577. height: 6px;
  578. }
  579. &::-webkit-scrollbar-track {
  580. background: var(--md-scroll-track);
  581. border-radius: 3px;
  582. }
  583. &::-webkit-scrollbar-thumb {
  584. background: var(--md-scroll-thumb);
  585. border-radius: 3px;
  586. }
  587. &::-webkit-scrollbar-thumb:hover {
  588. background: var(--md-scroll-thumb-hover);
  589. }
  590. }
  591. // 强制表格元素使用正确的display属性,防止被其他样式覆盖
  592. table {
  593. display: table !important;
  594. width: 100% !important;
  595. min-width: 480px !important;
  596. border-collapse: collapse !important;
  597. border-spacing: 0 !important;
  598. margin: 0 !important;
  599. font-size: 13px;
  600. table-layout: auto;
  601. }
  602. thead {
  603. display: table-header-group !important;
  604. }
  605. tbody {
  606. display: table-row-group !important;
  607. }
  608. tr {
  609. display: table-row !important;
  610. page-break-inside: avoid;
  611. }
  612. th {
  613. display: table-cell !important;
  614. background: var(--md-table-head-bg) !important;
  615. color: var(--md-table-head-text) !important;
  616. font-weight: 600 !important;
  617. padding: 10px 14px !important;
  618. border: 1px solid var(--md-table-border) !important;
  619. text-align: left !important;
  620. white-space: nowrap;
  621. position: relative;
  622. }
  623. td {
  624. display: table-cell !important;
  625. color: var(--md-table-cell-text) !important;
  626. padding: 10px 14px !important;
  627. border: 1px solid var(--md-table-border) !important;
  628. vertical-align: top !important;
  629. text-align: left !important;
  630. word-break: break-word;
  631. line-height: 1.6;
  632. }
  633. // 表格斑马纹效果
  634. tr:nth-child(even) td {
  635. background: var(--md-table-row-even) !important;
  636. }
  637. tr:nth-child(odd) td {
  638. background: transparent !important;
  639. }
  640. // 表格hover效果
  641. tr:hover td {
  642. background: var(--md-table-row-hover) !important;
  643. }
  644. // 表格内段落样式修正
  645. table p, td p, th p {
  646. margin: 0 !important;
  647. padding: 0 !important;
  648. }
  649. // 防止表格内容被转义成纯文本
  650. td, th {
  651. white-space: normal;
  652. }
  653. .markdown-score-radar {
  654. margin: 16px 0 8px;
  655. overflow: hidden;
  656. border: 1px solid var(--md-table-border);
  657. border-radius: 12px;
  658. background: var(--md-table-wrap-bg);
  659. }
  660. .markdown-score-radar__header {
  661. display: flex;
  662. align-items: center;
  663. justify-content: space-between;
  664. gap: 12px;
  665. padding: 14px 16px;
  666. border-bottom: 1px solid var(--md-table-border);
  667. strong, small { display: block; }
  668. strong { color: var(--md-heading); font-size: 15px; }
  669. small { margin-top: 2px; color: var(--md-text); opacity: .62; font-size: 11px; }
  670. }
  671. .markdown-score-radar__action {
  672. flex: 0 0 auto;
  673. padding: 7px 12px;
  674. border: 1px solid rgba(91, 152, 237, .7);
  675. border-radius: 8px;
  676. color: #fff;
  677. background: linear-gradient(135deg, #5b98ed, #4a72ca);
  678. font: inherit;
  679. font-size: 12px;
  680. cursor: pointer;
  681. }
  682. .markdown-score-radar__chart {
  683. display: block;
  684. width: min(100%, 560px);
  685. height: auto;
  686. margin: 4px auto 0;
  687. overflow: visible;
  688. }
  689. .markdown-score-radar__grid,
  690. .markdown-score-radar__axis {
  691. fill: none;
  692. stroke: var(--md-table-border);
  693. stroke-width: 1;
  694. }
  695. .markdown-score-radar__area {
  696. stroke-width: 2.2;
  697. stroke-linejoin: round;
  698. }
  699. .markdown-score-radar__area--ai { fill: rgba(62, 130, 219, .24); stroke: #4f9cff; }
  700. .markdown-score-radar__area--expert { fill: rgba(245, 158, 11, .18); stroke: #f59e0b; }
  701. .markdown-score-radar__point--ai { fill: #4f9cff; }
  702. .markdown-score-radar__point--expert { fill: #f59e0b; }
  703. .markdown-score-radar__label { fill: var(--md-text); font-size: 11px; }
  704. .markdown-score-radar__legend {
  705. display: flex;
  706. justify-content: center;
  707. gap: 20px;
  708. margin: -12px 0 12px;
  709. color: var(--md-text);
  710. font-size: 12px;
  711. span { display: inline-flex; align-items: center; gap: 6px; }
  712. }
  713. .markdown-score-radar__legend-dot {
  714. width: 9px;
  715. height: 9px;
  716. border-radius: 50%;
  717. }
  718. .markdown-score-radar__legend-dot--ai { background: #4f9cff; }
  719. .markdown-score-radar__legend-dot--expert { background: #f59e0b; }
  720. .markdown-score-radar__score-list {
  721. display: grid;
  722. grid-template-columns: repeat(2, minmax(0, 1fr));
  723. border-top: 1px solid var(--md-table-border);
  724. > div {
  725. display: grid;
  726. grid-template-columns: minmax(0, 1fr) auto auto;
  727. gap: 8px;
  728. padding: 8px 12px;
  729. border-right: 1px solid var(--md-table-border);
  730. border-bottom: 1px solid var(--md-table-border);
  731. color: var(--md-text);
  732. font-size: 11px;
  733. }
  734. span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  735. b { font-weight: 500; white-space: nowrap; }
  736. }
  737. @media (max-width: 620px) {
  738. .markdown-score-radar__header { align-items: flex-start; flex-direction: column; }
  739. .markdown-score-radar__score-list { grid-template-columns: 1fr; }
  740. .markdown-score-radar__label { font-size: 9px; }
  741. }
  742. }
  743. .markdown-score-dialog__hint {
  744. margin: 0 0 12px;
  745. color: #64748b;
  746. font-size: 13px;
  747. }
  748. .markdown-score-dialog__list {
  749. display: grid;
  750. gap: 10px;
  751. label {
  752. display: grid;
  753. grid-template-columns: minmax(0, 1fr) 120px;
  754. align-items: center;
  755. gap: 12px;
  756. }
  757. span { font-size: 13px; }
  758. }
  759. </style>