后端优化编排器报销查询和本体检测精度,增强报销单草稿保 存和附件回填逻辑,前端重构侧边栏组件支持折叠和图标导 航,完善文档中心状态筛选和详情提示,报销创建和审批详情 页优化会话管理和费用明细交互,新增助手应用服务和预设动 作工具函数,补充单元测试覆盖。
181 lines
5.7 KiB
JavaScript
181 lines
5.7 KiB
JavaScript
import MarkdownIt from 'markdown-it'
|
||
|
||
const markdown = new MarkdownIt({
|
||
html: false,
|
||
linkify: true,
|
||
breaks: true
|
||
})
|
||
|
||
const defaultTableOpen = markdown.renderer.rules.table_open
|
||
const defaultTableClose = markdown.renderer.rules.table_close
|
||
const defaultParagraphOpen = markdown.renderer.rules.paragraph_open
|
||
const defaultLinkOpen = markdown.renderer.rules.link_open
|
||
const defaultBlockquoteOpen = markdown.renderer.rules.blockquote_open
|
||
|
||
const RISK_TEXT_CLASS_BY_LABEL = {
|
||
低风险: 'markdown-risk-text-low',
|
||
中风险: 'markdown-risk-text-medium',
|
||
高风险: 'markdown-risk-text-high'
|
||
}
|
||
|
||
const ACTION_LINK_CLASS_BY_HREF = {
|
||
'#confirm-attachment-association': 'markdown-action-link-confirm',
|
||
'#application-submit': 'markdown-action-link-confirm',
|
||
'#review-next-step': 'markdown-action-link-next',
|
||
'#review-quick-edit': 'markdown-action-link-edit',
|
||
'#review-risk-panel': 'markdown-action-link-risk'
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
return String(text || '')
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
}
|
||
|
||
function renderRiskText(text) {
|
||
return escapeHtml(text).replace(/低风险|中风险|高风险/g, (label) => {
|
||
const className = RISK_TEXT_CLASS_BY_LABEL[label]
|
||
return className ? `<span class="${className}">${label}</span>` : label
|
||
})
|
||
}
|
||
|
||
function resolveActionLinkClass(href) {
|
||
const normalizedHref = String(href || '').trim()
|
||
return ACTION_LINK_CLASS_BY_HREF[normalizedHref] || ''
|
||
}
|
||
|
||
function inlineTokenHasActionLink(token) {
|
||
const children = Array.isArray(token?.children) ? token.children : []
|
||
return children.some((child) => (
|
||
child?.type === 'link_open' && resolveActionLinkClass(child.attrGet?.('href'))
|
||
))
|
||
}
|
||
|
||
function resolveInlineTokenPlainText(token) {
|
||
const children = Array.isArray(token?.children) ? token.children : []
|
||
const childText = children
|
||
.filter((child) => ['text', 'code_inline'].includes(String(child?.type || '')))
|
||
.map((child) => String(child?.content || ''))
|
||
.join('')
|
||
.trim()
|
||
return childText || String(token?.content || '').replace(/[*_`]+/g, '').trim()
|
||
}
|
||
|
||
function blockquoteHasAttachmentHeading(tokens, idx) {
|
||
for (let i = idx + 1; i < tokens.length; i += 1) {
|
||
const token = tokens[i]
|
||
if (token?.type === 'blockquote_close') {
|
||
return false
|
||
}
|
||
if (token?.type === 'inline') {
|
||
return /^附件\s*\d+\s*[::]/.test(resolveInlineTokenPlainText(token))
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
markdown.renderer.rules.paragraph_open = (tokens, idx, options, env, self) => {
|
||
if (inlineTokenHasActionLink(tokens[idx + 1])) {
|
||
tokens[idx].attrJoin('class', 'markdown-action-paragraph')
|
||
}
|
||
return defaultParagraphOpen
|
||
? defaultParagraphOpen(tokens, idx, options, env, self)
|
||
: self.renderToken(tokens, idx, options)
|
||
}
|
||
|
||
markdown.renderer.rules.link_open = (tokens, idx, options, env, self) => {
|
||
const actionClass = resolveActionLinkClass(tokens[idx].attrGet('href'))
|
||
if (actionClass) {
|
||
tokens[idx].attrJoin('class', `markdown-action-link ${actionClass}`)
|
||
}
|
||
return defaultLinkOpen
|
||
? defaultLinkOpen(tokens, idx, options, env, self)
|
||
: self.renderToken(tokens, idx, options)
|
||
}
|
||
|
||
markdown.renderer.rules.text = (tokens, idx) => renderRiskText(tokens[idx]?.content)
|
||
|
||
markdown.renderer.rules.blockquote_open = (tokens, idx, options, env, self) => {
|
||
if (blockquoteHasAttachmentHeading(tokens, idx)) {
|
||
tokens[idx].attrJoin('class', 'markdown-attachment-card')
|
||
}
|
||
return defaultBlockquoteOpen
|
||
? defaultBlockquoteOpen(tokens, idx, options, env, self)
|
||
: self.renderToken(tokens, idx, options)
|
||
}
|
||
|
||
markdown.renderer.rules.table_open = (tokens, idx, options, env, self) => (
|
||
`<div class="markdown-table-wrap">${defaultTableOpen ? defaultTableOpen(tokens, idx, options, env, self) : '<table>'}`
|
||
)
|
||
|
||
markdown.renderer.rules.table_close = (tokens, idx, options, env, self) => (
|
||
`${defaultTableClose ? defaultTableClose(tokens, idx, options, env, self) : '</table>'}</div>`
|
||
)
|
||
|
||
const ALLOWED_COLON_HEADING_TITLES = new Set([
|
||
'基础信息识别结果',
|
||
'报销测算参考',
|
||
'补充信息'
|
||
])
|
||
|
||
function splitColonHeadingLine(line) {
|
||
const rawLine = String(line || '')
|
||
const trimmed = rawLine.trim()
|
||
if (!trimmed || trimmed.startsWith('|') || /^#{1,6}\s/.test(trimmed)) {
|
||
return [rawLine]
|
||
}
|
||
|
||
const chineseColonIndex = trimmed.indexOf(':')
|
||
const asciiColonIndex = trimmed.indexOf(':')
|
||
const colonIndexes = [chineseColonIndex, asciiColonIndex].filter((index) => index > 0)
|
||
if (!colonIndexes.length) {
|
||
return [rawLine]
|
||
}
|
||
|
||
const colonIndex = Math.min(...colonIndexes)
|
||
const title = trimmed.slice(0, colonIndex + 1)
|
||
const titleText = title.slice(0, -1)
|
||
const body = trimmed.slice(colonIndex + 1).trim()
|
||
if (!ALLOWED_COLON_HEADING_TITLES.has(titleText)) {
|
||
return [rawLine]
|
||
}
|
||
|
||
return body ? [`### ${title}`, '', body] : [`### ${title}`]
|
||
}
|
||
|
||
function normalizeColonHeadings(text) {
|
||
const lines = String(text || '').replace(/\r\n?/g, '\n').split('\n')
|
||
const normalizedLines = []
|
||
let inFence = false
|
||
|
||
lines.forEach((line) => {
|
||
if (/^\s*(```|~~~)/.test(line)) {
|
||
inFence = !inFence
|
||
normalizedLines.push(line)
|
||
return
|
||
}
|
||
if (inFence) {
|
||
normalizedLines.push(line)
|
||
return
|
||
}
|
||
|
||
const nextLines = splitColonHeadingLine(line)
|
||
if (nextLines[0]?.startsWith('### ') && normalizedLines.length) {
|
||
const previousLine = normalizedLines[normalizedLines.length - 1]
|
||
if (String(previousLine || '').trim()) {
|
||
normalizedLines.push('')
|
||
}
|
||
}
|
||
normalizedLines.push(...nextLines)
|
||
})
|
||
|
||
return normalizedLines.join('\n').replace(/\n{3,}/g, '\n\n')
|
||
}
|
||
|
||
export function renderMarkdown(text = '') {
|
||
const normalized = normalizeColonHeadings(text).trim()
|
||
return normalized ? markdown.render(normalized) : ''
|
||
}
|