Files
effekt-interface-frontend/src/utils/bugStepsFormat.js
qiaoxinjiu dca942bc8f feat(test-platform): AI生成用例、业务技能配置与计划执行优化
- 用例管理增加 AI 生成用例 Tab、文档来源与技能/规则多选生成
- 新增业务技能与业务规则配置页及 API
- 计划执行列表展示模块路径与名称,移除 Jenkins URL 列

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 10:01:35 +08:00

193 lines
6.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** 复现步骤中的截图:存 Markdown 图片语法,详情页安全渲染 */
/** Markdown 图片:允许 ] 与 ( 之间有空格,与部分编辑器行为一致 */
export function getBugStepImageMarkdownRegex() {
return /!\[[^\]]*\]\s*\(\s*((?:https?:\/\/|\/)[^)\s]+)\s*\)/gi
}
/** 新建 / 无复现步骤数据时,富文本编辑器内的默认骨架 */
export const BUG_STEPS_DEFAULT_HTML =
'<p>复现步骤:</p>' +
'<p><br></p>' +
'<p><br></p>' +
'<p>实际结果:</p>' +
'<p><br></p>' +
'<p><br></p>' +
'<p>预期结果:</p>' +
'<p><br></p>'
export function escapeHtml(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
/** 用于 HTML 属性(如 img src */
export function escapeHtmlAttr(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
}
/**
* 上传文件路径中的反斜杠统一为 /
* @param {string} url
*/
export function normalizeUploadPathSlashes(url) {
return String(url || '').trim().replace(/\\/g, '/')
}
/**
* 静态资源Bug 上传图)访问根,不含末尾 /
* - 开发环境默认 http://localhost:8881与后端文件服务常见端口一致
* - 可在 index.html 里设置 window.__BUG_UPLOAD_ORIGIN__ 覆盖
* - 打包时可配置 VUE_APP_BUG_UPLOAD_ORIGIN需在 webpack DefinePlugin 中注入)
*/
export function getBugUploadStaticOrigin() {
if (typeof window !== 'undefined' && window.__BUG_UPLOAD_ORIGIN__) {
return String(window.__BUG_UPLOAD_ORIGIN__).replace(/\/$/, '')
}
try {
if (typeof process !== 'undefined' && process.env && process.env.VUE_APP_BUG_UPLOAD_ORIGIN) {
return String(process.env.VUE_APP_BUG_UPLOAD_ORIGIN).replace(/\/$/, '')
}
} catch (e) { /* ignore */ }
try {
if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'development') {
return 'http://localhost:8881'
}
} catch (e2) { /* ignore */ }
return ''
}
/**
* 浏览器实际加载图片用的地址:/uploads/... 在开发环境走 8881 端口
* 存库仍可为接口返回的完整 URL仅展示/预览时改写
*/
export function rewriteBugImageUrlForAccess(url) {
const u = normalizeUploadPathSlashes(url)
if (!u) return ''
const staticOrigin = getBugUploadStaticOrigin()
let path = ''
if (/^https?:\/\//i.test(u)) {
const dbl = u.indexOf('//')
const pathStart = u.indexOf('/', dbl >= 0 ? dbl + 2 : 0)
path = pathStart >= 0 ? u.slice(pathStart) : '/'
} else if (u.startsWith('/')) {
path = u
} else {
return u
}
path = normalizeUploadPathSlashes(path)
if (staticOrigin && path.indexOf('/uploads/') === 0) {
return staticOrigin + path
}
if (/^https?:\/\//i.test(u)) return u
return resolveUploadPublicUrl(path)
}
/** 相对路径补全为当前站点 origin无专用静态域时 */
export function resolveUploadPublicUrl(url) {
const u = String(url || '').trim()
if (!u) return ''
if (/^https?:\/\//i.test(u)) return u
if (u.startsWith('//')) return `${window.location.protocol}${u}`
if (u.startsWith('/')) return `${window.location.origin}${u}`
return u
}
/**
* 解析 POST /bug/upload 成功响应(与接口一致:{ code, message, data: { url } }
*/
export function parseBugUploadFileUrl(res) {
if (!res) return ''
const inner = res.data
if (inner && typeof inner === 'object' && inner.url != null && inner.url !== '') {
return String(inner.url).trim()
}
if (typeof inner === 'string' && (inner.startsWith('http') || inner.startsWith('/'))) {
return inner.trim()
}
if (typeof res === 'string' && (res.startsWith('http') || res.startsWith('/'))) {
return res.trim()
}
if (res.url != null && res.url !== '') {
return String(res.url).trim()
}
const legacy =
(inner && inner.fileUrl) ||
(inner && inner.file_url) ||
(inner && inner.path) ||
(inner && typeof inner.data === 'object' && inner.data && inner.data.url) ||
''
return legacy ? String(legacy).trim() : ''
}
/**
* 将步骤文本转为可展示的 HTML仅把 Markdown 图片转为 img其余转义
*/
export function formatBugStepsToHtml(raw) {
const s = String(raw || '')
const re = getBugStepImageMarkdownRegex()
let out = ''
let last = 0
let m
re.lastIndex = 0
while ((m = re.exec(s)) !== null) {
out += escapeHtml(s.slice(last, m.index)).replace(/\n/g, '<br>')
const rawUrl = normalizeUploadPathSlashes(m[1])
const displaySrc = rewriteBugImageUrlForAccess(rawUrl)
if (displaySrc && (/^https?:\/\//i.test(displaySrc) || displaySrc.startsWith('/'))) {
out += `<img class="bug-step-img" src="${escapeHtmlAttr(displaySrc)}" alt="截图" />`
} else {
out += escapeHtml(m[0])
}
last = m.index + m[0].length
}
out += escapeHtml(s.slice(last)).replace(/\n/g, '<br>')
return out
}
/** 判断是否为富文本 HTML与旧版纯文本 / Markdown 区分) */
export function isStepsLikelyHtml(s) {
return /<\s*(p|div|br|img|span|ul|ol|li|h[1-6]|table|strong|em)\b/i.test(String(s || '').trim())
}
/** 将 HTML 中 img 的 src 改写为可访问地址(开发环境 /uploads → 8881 */
export function rewriteImgSrcsInHtml(html) {
return String(html || '').replace(/(<img\b[^>]*\bsrc\s*=\s*)(["'])([^"']*)\2/gi, function (_m, pre, q, src) {
const fixed = rewriteBugImageUrlForAccess(normalizeUploadPathSlashes(src))
return pre + q + escapeHtmlAttr(fixed) + q
})
}
/**
* 旧数据(纯文本 / Markdown 图片)转为 wangEditor 可用的 HTML
*/
export function legacyStepsToEditorHtml(raw) {
const s = String(raw || '')
if (!s.trim()) return BUG_STEPS_DEFAULT_HTML
if (isStepsLikelyHtml(s)) return rewriteImgSrcsInHtml(s)
const re = getBugStepImageMarkdownRegex()
let out = ''
let last = 0
let m
re.lastIndex = 0
while ((m = re.exec(s)) !== null) {
const text = s.slice(last, m.index)
if (text) {
out += '<p>' + escapeHtml(text).replace(/\n/g, '<br/>') + '</p>'
}
const src = rewriteBugImageUrlForAccess(normalizeUploadPathSlashes(m[1]))
out += '<p><img src="' + escapeHtmlAttr(src) + '" style="max-width:100%;"/></p>'
last = m.index + m[0].length
}
const tail = s.slice(last)
if (tail) {
out += '<p>' + escapeHtml(tail).replace(/\n/g, '<br/>') + '</p>'
}
return out || BUG_STEPS_DEFAULT_HTML
}