功能更新:新增Bug管理模块,完善用户角色分配,优化项目设置
This commit is contained in:
73
src/utils/bugHistory.js
Normal file
73
src/utils/bugHistory.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import { addBugHistory } from '@/api/bugApi'
|
||||
|
||||
/** 将任意值转为接口可接受的字符串(空用 '') */
|
||||
export function toHistoryValue(v) {
|
||||
if (v === undefined || v === null) return ''
|
||||
if (typeof v === 'object') {
|
||||
try {
|
||||
return JSON.stringify(v)
|
||||
} catch (e) {
|
||||
return String(v)
|
||||
}
|
||||
}
|
||||
return String(v)
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入 Bug 操作历史(POST /bug/history/add)
|
||||
* 失败不抛错、不阻断主流程,仅静默忽略(可后续接日志)
|
||||
*/
|
||||
export function recordBugHistory(store, { bugId, fieldName, oldValue, newValue, operatorId }) {
|
||||
if (!bugId || !fieldName) return Promise.resolve()
|
||||
const uid =
|
||||
operatorId !== undefined && operatorId !== null && operatorId !== ''
|
||||
? operatorId
|
||||
: store && store.state && store.state.currentUser && store.state.currentUser.id
|
||||
if (uid === undefined || uid === null || uid === '') return Promise.resolve()
|
||||
return addBugHistory({
|
||||
bugId: Number(bugId),
|
||||
fieldName: String(fieldName),
|
||||
oldValue: toHistoryValue(oldValue),
|
||||
newValue: toHistoryValue(newValue),
|
||||
operatorId: Number(uid)
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
/** 编辑页:与接口 fieldName 对齐的表单字段(用于对比写历史) */
|
||||
export const BUG_EDIT_HISTORY_FIELDS = [
|
||||
{ formKey: 'title', fieldName: 'title' },
|
||||
{ formKey: 'bugType', fieldName: 'bug_type' },
|
||||
{ formKey: 'severity', fieldName: 'severity' },
|
||||
{ formKey: 'priority', fieldName: 'priority' },
|
||||
{ formKey: 'status', fieldName: 'status' },
|
||||
{ formKey: 'reporterId', fieldName: 'reporter_id' },
|
||||
{ formKey: 'assigneeId', fieldName: 'assignee_id' },
|
||||
{ formKey: 'moduleId', fieldName: 'module_id' },
|
||||
{ formKey: 'caseId', fieldName: 'case_id' },
|
||||
{ formKey: 'planId', fieldName: 'plan_id' },
|
||||
{ formKey: 'environment', fieldName: 'environment' },
|
||||
{ formKey: 'steps', fieldName: 'steps' },
|
||||
{ formKey: 'reproduceRate', fieldName: 'reproduce_rate' }
|
||||
]
|
||||
|
||||
export function buildBugEditBaseline(form) {
|
||||
const f = form || {}
|
||||
const o = {}
|
||||
BUG_EDIT_HISTORY_FIELDS.forEach(({ formKey }) => {
|
||||
o[formKey] = f[formKey]
|
||||
})
|
||||
return o
|
||||
}
|
||||
|
||||
/** 对比编辑前后并逐字段写历史(仅变更字段) */
|
||||
export function recordBugEditDiff(store, bugId, baseline, current) {
|
||||
if (!bugId || !baseline || !current) return Promise.resolve()
|
||||
const tasks = []
|
||||
BUG_EDIT_HISTORY_FIELDS.forEach(({ formKey, fieldName }) => {
|
||||
const ov = baseline[formKey]
|
||||
const nv = current[formKey]
|
||||
if (toHistoryValue(ov) === toHistoryValue(nv)) return
|
||||
tasks.push(recordBugHistory(store, { bugId, fieldName, oldValue: ov, newValue: nv }))
|
||||
})
|
||||
return Promise.all(tasks)
|
||||
}
|
||||
163
src/utils/bugHistoryDisplay.js
Normal file
163
src/utils/bugHistoryDisplay.js
Normal file
@@ -0,0 +1,163 @@
|
||||
import {
|
||||
STATUS_MAP,
|
||||
PRIORITY_MAP,
|
||||
SEVERITY_MAP,
|
||||
BUG_TYPE_MAP,
|
||||
formatBugType,
|
||||
formatReproduceRate
|
||||
} from '@/utils/bugMaps'
|
||||
|
||||
/** 历史记录 field_name / fieldName → 中文 */
|
||||
const FIELD_NAME_CN = {
|
||||
status: '状态',
|
||||
assignee_id: '当前指派',
|
||||
reporter_id: '创建人',
|
||||
module_id: '模块',
|
||||
product_id: '产品',
|
||||
project_id: '项目',
|
||||
priority: '优先级',
|
||||
severity: '严重程度',
|
||||
bug_type: '类型',
|
||||
title: '标题',
|
||||
environment: '环境',
|
||||
case_id: '关联用例',
|
||||
plan_id: '关联计划',
|
||||
steps: '复现步骤',
|
||||
solution: '解决方案',
|
||||
resolve_version: '解决版本',
|
||||
resolve_comment: '解决备注',
|
||||
comment: '备注',
|
||||
create: '创建',
|
||||
delete: '删除',
|
||||
description: '描述',
|
||||
user_id: '用户',
|
||||
is_auto: '自动化',
|
||||
reproduce_rate: '复现率',
|
||||
resolved_by: '解决人'
|
||||
}
|
||||
|
||||
const SOLUTION_CN = {
|
||||
by_design: '设计如此',
|
||||
duplicate_bug: '重复Bug',
|
||||
external_reason: '外部原因',
|
||||
solution_resolved: '已解决',
|
||||
cannot_reproduce: '无法重现',
|
||||
deferred: '延期处理',
|
||||
wont_fix: '不予解决'
|
||||
}
|
||||
|
||||
const RESOLVE_VERSION_CN = {
|
||||
trunk: '主干',
|
||||
current_sprint: '当前迭代',
|
||||
next: '下一版本'
|
||||
}
|
||||
|
||||
function stripHtmlBrief(html, maxLen) {
|
||||
if (html == null || html === '') return ''
|
||||
const text = String(html)
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
if (!text) return '(富文本)'
|
||||
if (maxLen && text.length > maxLen) return text.slice(0, maxLen) + '…'
|
||||
return text
|
||||
}
|
||||
|
||||
function toSnakeFieldKey(name) {
|
||||
const s = String(name || '').trim()
|
||||
if (!s) return ''
|
||||
return s
|
||||
.replace(/([a-z\d])([A-Z])/g, '$1_$2')
|
||||
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
||||
.toLowerCase()
|
||||
.replace(/^_+/, '')
|
||||
.replace(/_+/g, '_')
|
||||
}
|
||||
|
||||
/** 与 BUG_EDIT_HISTORY_FIELDS / 接口 fieldName 对齐的 snake_case key */
|
||||
export function normalizeBugHistoryFieldKey(name) {
|
||||
const snake = toSnakeFieldKey(name)
|
||||
if (!snake) return ''
|
||||
const compact = snake.replace(/_/g, '')
|
||||
const alias = {
|
||||
assigneeid: 'assignee_id',
|
||||
reporterid: 'reporter_id',
|
||||
moduleid: 'module_id',
|
||||
productid: 'product_id',
|
||||
projectid: 'project_id',
|
||||
caseid: 'case_id',
|
||||
planid: 'plan_id',
|
||||
bugtype: 'bug_type',
|
||||
reproducerate: 'reproduce_rate',
|
||||
resolvedby: 'resolved_by',
|
||||
userid: 'user_id',
|
||||
isauto: 'is_auto',
|
||||
resolveversion: 'resolve_version',
|
||||
resolvecomment: 'resolve_comment',
|
||||
oldvalue: 'old_value',
|
||||
newvalue: 'new_value',
|
||||
fieldname: 'field_name'
|
||||
}
|
||||
return alias[compact] || snake
|
||||
}
|
||||
|
||||
export function formatBugHistoryFieldName(name) {
|
||||
const k = normalizeBugHistoryFieldKey(name)
|
||||
if (!k) return '-'
|
||||
return FIELD_NAME_CN[k] || String(name || '').trim() || '-'
|
||||
}
|
||||
|
||||
/**
|
||||
* 按字段将历史 old/new 转为中文展示(未知枚举则原样返回)
|
||||
*/
|
||||
export function formatBugHistoryCellValue(fieldName, value) {
|
||||
const fn = normalizeBugHistoryFieldKey(fieldName)
|
||||
const raw = value
|
||||
if (raw === undefined || raw === null || raw === '') return '(空)'
|
||||
|
||||
const s = String(raw).trim()
|
||||
|
||||
switch (fn) {
|
||||
case 'status':
|
||||
return STATUS_MAP[Number(s)] != null ? STATUS_MAP[Number(s)] : s
|
||||
case 'priority':
|
||||
return PRIORITY_MAP[Number(s)] != null ? PRIORITY_MAP[Number(s)] : s
|
||||
case 'severity':
|
||||
return SEVERITY_MAP[Number(s)] != null ? SEVERITY_MAP[Number(s)] : s
|
||||
case 'bug_type':
|
||||
return BUG_TYPE_MAP[Number(s)] != null ? BUG_TYPE_MAP[Number(s)] : formatBugType(Number(s))
|
||||
case 'solution':
|
||||
return SOLUTION_CN[s] || s
|
||||
case 'resolve_version':
|
||||
return RESOLVE_VERSION_CN[s] || s
|
||||
case 'resolve_comment':
|
||||
case 'comment':
|
||||
return stripHtmlBrief(raw, 160)
|
||||
case 'create':
|
||||
case 'delete':
|
||||
if (s === '0' || s === '1') return s === '1' ? '是' : '否'
|
||||
return s
|
||||
case 'steps':
|
||||
return stripHtmlBrief(raw, 120)
|
||||
case 'reproduce_rate':
|
||||
return formatReproduceRate(s)
|
||||
case 'resolved_by':
|
||||
return s
|
||||
default:
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
/** 解决弹窗历史列表用:单行描述 */
|
||||
export function formatBugHistoryLineText(h) {
|
||||
const fn = h.field_name || h.fieldName || ''
|
||||
var ov = h.old_value
|
||||
if (ov === undefined || ov === null) ov = h.oldValue
|
||||
var nv = h.new_value
|
||||
if (nv === undefined || nv === null) nv = h.newValue
|
||||
const op = h.operator_id || h.operatorId || h.operator_name || h.operatorName || ''
|
||||
const label = formatBugHistoryFieldName(fn)
|
||||
const o = formatBugHistoryCellValue(fn, ov)
|
||||
const n = formatBugHistoryCellValue(fn, nv)
|
||||
return `${label}:${o} → ${n}(操作人 ${op})`
|
||||
}
|
||||
143
src/utils/bugMaps.js
Normal file
143
src/utils/bugMaps.js
Normal file
@@ -0,0 +1,143 @@
|
||||
/** Bug 枚举与展示(与接口一致:bug_type 1–11) */
|
||||
|
||||
export const BUG_TYPE_MAP = {
|
||||
1: '功能缺陷',
|
||||
2: 'UI问题',
|
||||
3: '性能问题',
|
||||
4: '安全漏洞',
|
||||
5: '兼容性问题',
|
||||
6: '前端bug',
|
||||
7: '后端bug',
|
||||
8: '配置问题',
|
||||
9: '产品设计缺陷',
|
||||
10: '产品优化',
|
||||
11: '其他'
|
||||
}
|
||||
|
||||
export const SEVERITY_MAP = {
|
||||
1: '致命',
|
||||
2: '严重',
|
||||
3: '中等',
|
||||
4: '轻微'
|
||||
}
|
||||
|
||||
export const PRIORITY_MAP = {
|
||||
1: '高',
|
||||
2: '中',
|
||||
3: '低'
|
||||
}
|
||||
|
||||
/** 复现概率 / 复现率(与接口 reproduceRate 一致) */
|
||||
export const REPRODUCE_RATE_MAP = {
|
||||
1: '必现',
|
||||
2: '偶现',
|
||||
3: '仅一次',
|
||||
4: '难重现'
|
||||
}
|
||||
|
||||
export function formatReproduceRate(v) {
|
||||
const n = Number(v)
|
||||
if (Number.isNaN(n)) return v === undefined || v === null || v === '' ? '-' : String(v)
|
||||
return REPRODUCE_RATE_MAP[n] != null ? REPRODUCE_RATE_MAP[n] : String(v)
|
||||
}
|
||||
|
||||
export const STATUS_MAP = {
|
||||
0: '新建',
|
||||
1: '待处理',
|
||||
2: '进行中',
|
||||
3: '已解决',
|
||||
4: '已关闭',
|
||||
5: '已拒绝'
|
||||
}
|
||||
|
||||
/** 已解决状态值(与接口一致) */
|
||||
export const BUG_STATUS_RESOLVED = 3
|
||||
|
||||
/** 已关闭 */
|
||||
export const BUG_STATUS_CLOSED = 4
|
||||
|
||||
/** 待处理(重新打开目标状态) */
|
||||
export const BUG_STATUS_PENDING = 1
|
||||
|
||||
/** 已拒绝 */
|
||||
export const BUG_STATUS_REJECTED = 5
|
||||
|
||||
/**
|
||||
* 主流程下一状态:新建→待处理→进行中→已解决→已关闭;已拒绝→待处理。
|
||||
* 已关闭、未知状态无下一档。
|
||||
*/
|
||||
export function getBugStatusNextTransition(status) {
|
||||
const s = Number(status)
|
||||
if (Number.isNaN(s)) return null
|
||||
const nextMap = {
|
||||
0: 1,
|
||||
1: 2,
|
||||
2: 3,
|
||||
3: 4,
|
||||
5: 1
|
||||
}
|
||||
const next = nextMap[s]
|
||||
if (next === undefined) return null
|
||||
const label = STATUS_MAP[next]
|
||||
if (label == null) return null
|
||||
return { nextStatus: next, nextLabel: label }
|
||||
}
|
||||
|
||||
export function formatBugType(v) {
|
||||
return BUG_TYPE_MAP[v] != null ? BUG_TYPE_MAP[v] : v
|
||||
}
|
||||
|
||||
export function formatSeverity(v) {
|
||||
return SEVERITY_MAP[v] != null ? SEVERITY_MAP[v] : v
|
||||
}
|
||||
|
||||
export function formatPriority(v) {
|
||||
return PRIORITY_MAP[v] != null ? PRIORITY_MAP[v] : v
|
||||
}
|
||||
|
||||
export function formatStatus(v) {
|
||||
return STATUS_MAP[v] != null ? STATUS_MAP[v] : v
|
||||
}
|
||||
|
||||
/** ElementUI el-tag type */
|
||||
export function statusTagType(status) {
|
||||
const map = {
|
||||
0: 'info',
|
||||
1: 'warning',
|
||||
2: 'primary',
|
||||
3: 'success',
|
||||
4: 'info',
|
||||
5: 'danger'
|
||||
}
|
||||
return map[Number(status)] || 'info'
|
||||
}
|
||||
|
||||
/** 详情页等:状态角标独立配色(class 名) */
|
||||
export function statusBadgeClass(status) {
|
||||
const map = {
|
||||
0: 'bug-status-new',
|
||||
1: 'bug-status-pending',
|
||||
2: 'bug-status-progress',
|
||||
3: 'bug-status-resolved',
|
||||
4: 'bug-status-closed',
|
||||
5: 'bug-status-rejected'
|
||||
}
|
||||
return map[Number(status)] != null ? map[Number(status)] : 'bug-status-unknown'
|
||||
}
|
||||
|
||||
export function severityTagType(severity) {
|
||||
const map = { 1: 'danger', 2: 'warning', 3: '', 4: 'info' }
|
||||
return map[Number(severity)] || 'info'
|
||||
}
|
||||
|
||||
export function priorityTagType(priority) {
|
||||
const map = { 1: 'danger', 2: 'warning', 3: 'success' }
|
||||
return map[Number(priority)] || 'info'
|
||||
}
|
||||
|
||||
export function bugTypeTagType(bugType) {
|
||||
const n = Number(bugType)
|
||||
if (!n || n < 1) return ''
|
||||
const cycle = ['', 'success', 'warning', 'danger', 'info']
|
||||
return cycle[(n - 1) % cycle.length]
|
||||
}
|
||||
192
src/utils/bugStepsFormat.js
Normal file
192
src/utils/bugStepsFormat.js
Normal file
@@ -0,0 +1,192 @@
|
||||
/** 复现步骤中的截图:存 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
/** 用于 HTML 属性(如 img src) */
|
||||
export function escapeHtmlAttr(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件路径中的反斜杠统一为 /
|
||||
* @param {string} url
|
||||
*/
|
||||
export function normalizeUploadPathSlashes(url) {
|
||||
return String(url || '').trim().replace(/\\/g, '/')
|
||||
}
|
||||
|
||||
/**
|
||||
* 静态资源(Bug 上传图)访问根,不含末尾 /
|
||||
* - 开发环境默认 http://localhost:5010(与后端文件服务常见端口一致)
|
||||
* - 可在 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:5010'
|
||||
}
|
||||
} catch (e2) { /* ignore */ }
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览器实际加载图片用的地址:/uploads/... 在开发环境走 5010 端口
|
||||
* 存库仍可为接口返回的完整 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 → 5010) */
|
||||
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
|
||||
}
|
||||
39
src/utils/lastProductProjectCache.js
Normal file
39
src/utils/lastProductProjectCache.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 全局「最近选择的产品 + 项目」缓存(localStorage),供 Bug 列表、用例、计划、报告等共用。
|
||||
* 兼容旧 key:effekt_bug_list_last_product_project
|
||||
*/
|
||||
export const LAST_PRODUCT_PROJECT_CACHE_KEY = 'effekt_last_product_project'
|
||||
|
||||
const LEGACY_BUG_LIST_CACHE_KEY = 'effekt_bug_list_last_product_project'
|
||||
|
||||
export function readLastProductProjectCache() {
|
||||
try {
|
||||
let raw = localStorage.getItem(LAST_PRODUCT_PROJECT_CACHE_KEY)
|
||||
if (!raw) raw = localStorage.getItem(LEGACY_BUG_LIST_CACHE_KEY)
|
||||
if (!raw) return null
|
||||
const o = JSON.parse(raw)
|
||||
if (!o || typeof o !== 'object') return null
|
||||
return { productId: o.productId, projectId: o.projectId }
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function saveLastProductProjectCache(productId, projectId) {
|
||||
if (productId === '' || productId === undefined || productId === null) return
|
||||
if (projectId === '' || projectId === undefined || projectId === null) return
|
||||
try {
|
||||
const payload = JSON.stringify({ productId, projectId })
|
||||
localStorage.setItem(LAST_PRODUCT_PROJECT_CACHE_KEY, payload)
|
||||
if (localStorage.getItem(LEGACY_BUG_LIST_CACHE_KEY)) {
|
||||
localStorage.removeItem(LEGACY_BUG_LIST_CACHE_KEY)
|
||||
}
|
||||
} catch (e) {
|
||||
// quota / 隐私模式等忽略
|
||||
}
|
||||
}
|
||||
|
||||
export function pickIdFromOptions(options, rawId) {
|
||||
const row = (options || []).find(p => String(p.id) === String(rawId))
|
||||
return row ? row.id : rawId
|
||||
}
|
||||
@@ -30,10 +30,17 @@ service.interceptors.response.use(
|
||||
// 兼容后端返回结构:{ success, code, message, data }
|
||||
if (data && data.code === 500) {
|
||||
Message.error('服务异常')
|
||||
return Promise.reject(new Error(data.message || '服务异常'))
|
||||
} else if (data && data.code === 451) {
|
||||
router.push({ name: 'login' })
|
||||
Message.error(data.message || '登录已失效,请重新登录')
|
||||
return Promise.reject(new Error(data.message || '登录已失效'))
|
||||
} else if (data && data.success === false) {
|
||||
Message.error(data.message || '请求失败')
|
||||
return Promise.reject(new Error(data.message || '请求失败'))
|
||||
} else if (data && data.code !== undefined && data.code !== 20000) {
|
||||
Message.error(data.message || '请求失败')
|
||||
return Promise.reject(new Error(data.message || '请求失败'))
|
||||
} else {
|
||||
return response.data
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user