1499 lines
46 KiB
Vue
1499 lines
46 KiB
Vue
<template>
|
||
<div class="page-wrap page-wrap--bug-detail page-wrap--fixed-actions">
|
||
<page-section title="Bug 详情">
|
||
<template slot="extra">
|
||
<el-button size="small" @click="goBack">返回列表</el-button>
|
||
<el-button type="primary" size="small" @click="goEdit">编辑</el-button>
|
||
</template>
|
||
<div class="detail-layout">
|
||
<div v-loading="loading" class="detail-top-loading">
|
||
<el-row :gutter="20">
|
||
<el-col :span="24">
|
||
<div class="detail-card main-card">
|
||
<div class="main-card-head">
|
||
<div class="main-card-head-left">
|
||
<span class="bug-key">{{ detail.bug_key || detail.bugKey || '' }}</span>
|
||
<el-tag size="small" :type="severityTagType(detail.severity)">{{ formatSeverity(detail.severity) }}</el-tag>
|
||
<el-tag size="small" :type="priorityTagType(detail.priority)">{{ formatPriority(detail.priority) }}</el-tag>
|
||
</div>
|
||
<div class="status-corner" :class="statusBadgeClass(detail.status)">
|
||
{{ formatStatus(detail.status) }}
|
||
</div>
|
||
</div>
|
||
<h2 class="bug-title">{{ detail.title }}</h2>
|
||
<el-row :gutter="16" class="meta-rows">
|
||
<el-col :span="8">
|
||
<div class="meta-field">
|
||
<div class="meta-label">缺陷类型</div>
|
||
<div class="meta-value">{{ formatBugType(detailBugType) }}</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<div class="meta-field">
|
||
<div class="meta-label">环境</div>
|
||
<div class="meta-value">{{ detail.environment || '-' }}</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<div class="meta-field">
|
||
<div class="meta-label">当前指派</div>
|
||
<div class="meta-value">{{ assigneeDisplay }}</div>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="16" class="meta-rows">
|
||
<el-col :span="8">
|
||
<div class="meta-field">
|
||
<div class="meta-label">创建人</div>
|
||
<div class="meta-value">{{ reporterDisplay }}</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<div class="meta-field">
|
||
<div class="meta-label">产品</div>
|
||
<div class="meta-value">{{ productDisplay }}</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<div class="meta-field">
|
||
<div class="meta-label">项目</div>
|
||
<div class="meta-value">{{ projectDisplay }}</div>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="16" class="meta-rows">
|
||
<el-col :span="8">
|
||
<div class="meta-field">
|
||
<div class="meta-label">模块</div>
|
||
<div class="meta-value">{{ moduleDisplay }}</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<div class="meta-field">
|
||
<div class="meta-label">关联用例</div>
|
||
<div class="meta-value">{{ detail.case_id || detail.caseId || '-' }}</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<div class="meta-field">
|
||
<div class="meta-label">关联计划</div>
|
||
<div class="meta-value">{{ detail.plan_id || detail.planId || '-' }}</div>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="16" class="meta-rows meta-rows-last">
|
||
<el-col :span="8">
|
||
<div class="meta-field">
|
||
<div class="meta-label">创建时间</div>
|
||
<div class="meta-value">{{ formatTime(detail.created_time || detail.createdTime) }}</div>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
|
||
<div class="detail-card steps-card mt">
|
||
<div
|
||
v-if="hasStepsContent"
|
||
class="steps-display"
|
||
v-html="stepsDisplayHtml"
|
||
></div>
|
||
<div v-else class="steps-empty">暂无复现内容</div>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
|
||
<div class="detail-card mt feed-tab-card">
|
||
<div class="feed-pill-tabs" role="tablist">
|
||
<button
|
||
type="button"
|
||
class="feed-pill-tab"
|
||
:class="{ 'is-active': feedMainTab === 'comments' }"
|
||
role="tab"
|
||
:aria-selected="feedMainTab === 'comments'"
|
||
@click="feedMainTab = 'comments'">
|
||
评论
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="feed-pill-tab"
|
||
:class="{ 'is-active': feedMainTab === 'history' }"
|
||
role="tab"
|
||
:aria-selected="feedMainTab === 'history'"
|
||
@click="feedMainTab = 'history'">
|
||
变更历史
|
||
</button>
|
||
</div>
|
||
<div class="feed-tab-divider"></div>
|
||
|
||
<div v-show="feedMainTab === 'comments'" class="feed-tab-panel" role="tabpanel">
|
||
<div class="comment-tab-toolbar">
|
||
<el-button
|
||
type="primary"
|
||
plain
|
||
size="small"
|
||
class="comment-add-remark-btn"
|
||
icon="el-icon-edit-outline"
|
||
@click="openCommentDialog">
|
||
添加备注
|
||
</el-button>
|
||
</div>
|
||
<div v-for="c in commentList" :key="c.id" class="comment-item">
|
||
<div class="comment-meta">
|
||
<span>{{ (c.user_name || c.userName || c.username || '-') }}:</span>
|
||
<span class="time">{{ formatTime(c.created_time || c.createdTime) }}</span>
|
||
</div>
|
||
<div class="comment-body" v-html="c.content || ''"></div>
|
||
</div>
|
||
<div v-if="!commentList.length" class="empty-hint">暂无评论</div>
|
||
</div>
|
||
|
||
<div v-show="feedMainTab === 'history'" class="feed-tab-panel" role="tabpanel">
|
||
<el-timeline v-if="historyList.length">
|
||
<el-timeline-item v-for="h in historyList" :key="h.id" :timestamp="formatTime(h.created_time || h.createdTime)" placement="top">
|
||
<div class="history-line">
|
||
<span class="history-field">{{ formatHistoryFieldName(h.field_name || h.fieldName) }}</span>
|
||
<span class="history-arrow">
|
||
<template v-if="historyFieldUsesColoredTags(h)">
|
||
<span class="history-tag-pair">
|
||
<el-tag
|
||
size="mini"
|
||
:type="historyTagType(historyNormKey(h), h.old_value || h.oldValue)"
|
||
:effect="historyTagEffect(h.old_value || h.oldValue)"
|
||
class="history-enum-tag">
|
||
{{ formatHistoryCellValue(h.field_name || h.fieldName, h.old_value || h.oldValue) }}
|
||
</el-tag>
|
||
<span class="history-arrow-symbol">→</span>
|
||
<el-tag
|
||
size="mini"
|
||
:type="historyTagType(historyNormKey(h), h.new_value || h.newValue)"
|
||
:effect="historyTagEffect(h.new_value || h.newValue)"
|
||
class="history-enum-tag">
|
||
{{ formatHistoryCellValue(h.field_name || h.fieldName, h.new_value || h.newValue) }}
|
||
</el-tag>
|
||
</span>
|
||
</template>
|
||
<template v-else>
|
||
{{ formatHistoryCellValue(h.field_name || h.fieldName, h.old_value || h.oldValue) }}
|
||
<span class="history-arrow-symbol history-arrow-symbol--text">→</span>
|
||
{{ formatHistoryCellValue(h.field_name || h.fieldName, h.new_value || h.newValue) }}
|
||
</template>
|
||
</span>
|
||
<span class="history-op">操作人 {{ historyOperatorLabel(h) }}</span>
|
||
</div>
|
||
</el-timeline-item>
|
||
</el-timeline>
|
||
<div v-else class="empty-hint">暂无历史记录</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</page-section>
|
||
|
||
<div v-if="effectiveBugId" class="bug-action-dock">
|
||
<div class="bug-action-dock-inner">
|
||
<div class="bug-action-strip">
|
||
<el-button
|
||
v-if="nextStatusTransition"
|
||
type="primary"
|
||
size="small"
|
||
:loading="actionSaving"
|
||
:disabled="loading"
|
||
@click="applyNextStatus">
|
||
{{ nextStatusTransition.nextLabel }}
|
||
</el-button>
|
||
<span v-if="nextStatusTransition" class="bug-action-strip-divider" />
|
||
<el-button
|
||
size="small"
|
||
plain
|
||
:disabled="loading || cannotUseResolveButton"
|
||
@click="openResolveDialog">
|
||
解决
|
||
</el-button>
|
||
<span class="bug-action-strip-divider" />
|
||
<el-button
|
||
size="small"
|
||
plain
|
||
:loading="reopenSaving"
|
||
:disabled="loading || !canReopenBug"
|
||
@click="applyReopen">
|
||
重新打开
|
||
</el-button>
|
||
<span class="bug-action-strip-divider" />
|
||
<el-button
|
||
size="small"
|
||
type="danger"
|
||
plain
|
||
:loading="deleteSaving"
|
||
:disabled="loading"
|
||
@click="confirmDelete">
|
||
删除
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<el-dialog
|
||
:visible.sync="resolveDialogVisible"
|
||
width="800px"
|
||
append-to-body
|
||
destroy-on-close
|
||
custom-class="bug-resolve-dialog"
|
||
:close-on-click-modal="false"
|
||
@closed="resetResolveForm">
|
||
<div slot="title" class="resolve-dialog-header">
|
||
<span class="resolve-dialog-id">{{ detail.id || detail.bug_id || detail.bugId || bugId }}</span>
|
||
<span class="resolve-dialog-title-text">{{ detail.title || '解决 Bug' }}</span>
|
||
</div>
|
||
<el-form
|
||
ref="resolveFormRef"
|
||
:model="resolveForm"
|
||
:rules="resolveFormRules"
|
||
label-width="108px"
|
||
label-position="right"
|
||
size="small"
|
||
class="resolve-dialog-form">
|
||
<el-form-item label="解决方案" prop="solutionType">
|
||
<el-select
|
||
v-model="resolveForm.solutionType"
|
||
placeholder="请选择"
|
||
class="resolve-field-select"
|
||
popper-class="bug-resolve-solution-dropdown"
|
||
style="width: 100%;">
|
||
<el-option
|
||
v-for="opt in solutionOptions"
|
||
:key="opt.value"
|
||
:label="opt.label"
|
||
:value="opt.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="解决版本">
|
||
<el-select
|
||
v-model="resolveForm.fixedVersion"
|
||
clearable
|
||
placeholder="请选择"
|
||
class="resolve-field-select"
|
||
style="width: 100%;">
|
||
<el-option
|
||
v-for="opt in versionOptions"
|
||
:key="opt.value"
|
||
:label="opt.label"
|
||
:value="opt.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="解决日期" prop="resolvedAt">
|
||
<el-date-picker
|
||
v-model="resolveForm.resolvedAt"
|
||
type="datetime"
|
||
value-format="yyyy-MM-dd HH:mm:ss"
|
||
format="yyyy-MM-dd HH:mm:ss"
|
||
placeholder="选择日期时间"
|
||
style="width: 100%;" />
|
||
</el-form-item>
|
||
<el-form-item label="当前指派" prop="assigneeId">
|
||
<el-select
|
||
v-model="resolveForm.assigneeId"
|
||
filterable
|
||
clearable
|
||
placeholder="请选择"
|
||
style="width: 100%;"
|
||
@focus="onResolveAssigneeFocus">
|
||
<el-option
|
||
v-for="u in resolveMemberOptions"
|
||
:key="'rm-' + u.id"
|
||
:label="u.name"
|
||
:value="u.id" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="解决人">
|
||
<span class="resolve-resolver-readonly">{{ currentResolverDisplay }}</span>
|
||
<span class="resolve-resolver-hint">(默认当前登录用户)</span>
|
||
</el-form-item>
|
||
<el-form-item label="备注" class="resolve-remark-item">
|
||
<bug-steps-rich-editor :key="'resolve-editor-' + resolveDialogKey" v-model="resolveForm.remarkHtml" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<div class="resolve-dialog-footer">
|
||
<el-button type="primary" :loading="resolveDialogSaving" @click="submitResolveDialog">保存</el-button>
|
||
</div>
|
||
<div class="resolve-history-block">
|
||
<div class="resolve-history-title">历史记录</div>
|
||
<ol v-if="resolveHistoryPreviewList.length" class="resolve-history-list">
|
||
<li v-for="(row, idx) in resolveHistoryPreviewList" :key="'rh-' + (row.raw && row.raw.id != null ? row.raw.id : idx)">
|
||
<span class="resolve-history-time">{{ row.time }}</span>,
|
||
<span class="history-field">{{ formatHistoryFieldName(row.fieldKey) }}</span>
|
||
<span class="history-arrow history-arrow--inline">
|
||
<template v-if="historyFieldKeyUsesColoredTags(row.fieldKey)">
|
||
<span class="history-tag-pair">
|
||
<el-tag
|
||
size="mini"
|
||
:type="historyTagType(row.fieldKey, row.oldVal)"
|
||
:effect="historyTagEffect(row.oldVal)"
|
||
class="history-enum-tag">
|
||
{{ formatHistoryCellValue(row.fieldKey, row.oldVal) }}
|
||
</el-tag>
|
||
<span class="history-arrow-symbol">→</span>
|
||
<el-tag
|
||
size="mini"
|
||
:type="historyTagType(row.fieldKey, row.newVal)"
|
||
:effect="historyTagEffect(row.newVal)"
|
||
class="history-enum-tag">
|
||
{{ formatHistoryCellValue(row.fieldKey, row.newVal) }}
|
||
</el-tag>
|
||
</span>
|
||
</template>
|
||
<template v-else>
|
||
{{ formatHistoryCellValue(row.fieldKey, row.oldVal) }}
|
||
<span class="history-arrow-symbol history-arrow-symbol--text">→</span>
|
||
{{ formatHistoryCellValue(row.fieldKey, row.newVal) }}
|
||
</template>
|
||
</span>
|
||
<span class="resolve-history-op">(操作人 {{ row.op }})</span>
|
||
</li>
|
||
</ol>
|
||
<div v-else class="resolve-history-empty">暂无历史记录</div>
|
||
</div>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
title="添加备注"
|
||
:visible.sync="commentDialogVisible"
|
||
width="900px"
|
||
append-to-body
|
||
custom-class="bug-comment-dialog"
|
||
:close-on-click-modal="false"
|
||
@closed="resetCommentDialog">
|
||
<bug-steps-rich-editor
|
||
:key="'comment-editor-' + commentDialogKey"
|
||
v-model="commentRemarkHtml"
|
||
:height="320"
|
||
placeholder="请输入备注内容,工具栏可插入图片,支持粘贴截图" />
|
||
<div slot="footer" class="bug-comment-dialog-footer">
|
||
<el-button @click="commentDialogVisible = false">关闭</el-button>
|
||
<el-button type="primary" :loading="commentSubmitting" @click="submitCommentFromDialog">保存</el-button>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import PageSection from '@/components/TestPlatform/common/PageSection'
|
||
import BugStepsRichEditor from '@/components/Bug/BugStepsRichEditor.vue'
|
||
import { getBugDetail, updateBug, addBugComment, deleteBug } from '@/api/bugApi'
|
||
import { recordBugHistory } from '@/utils/bugHistory'
|
||
import {
|
||
formatBugHistoryFieldName,
|
||
formatBugHistoryCellValue,
|
||
normalizeBugHistoryFieldKey
|
||
} from '@/utils/bugHistoryDisplay'
|
||
import { getProductList } from '@/api/productApi'
|
||
import { getProjectDetail, getProjectMembers } from '@/api/projectApi'
|
||
import { getModuleTree } from '@/api/caseApi'
|
||
import {
|
||
formatBugType,
|
||
formatSeverity,
|
||
formatPriority,
|
||
formatStatus,
|
||
statusBadgeClass,
|
||
getBugStatusNextTransition,
|
||
BUG_STATUS_RESOLVED,
|
||
BUG_STATUS_CLOSED,
|
||
BUG_STATUS_PENDING,
|
||
BUG_STATUS_REJECTED,
|
||
severityTagType,
|
||
priorityTagType,
|
||
statusTagType
|
||
} from '@/utils/bugMaps'
|
||
import {
|
||
formatBugStepsToHtml,
|
||
isStepsLikelyHtml,
|
||
rewriteImgSrcsInHtml
|
||
} from '@/utils/bugStepsFormat'
|
||
|
||
function getDefaultResolveForm() {
|
||
const now = new Date()
|
||
const pad = n => (n < 10 ? '0' + n : '' + n)
|
||
const ts =
|
||
now.getFullYear() +
|
||
'-' +
|
||
pad(now.getMonth() + 1) +
|
||
'-' +
|
||
pad(now.getDate()) +
|
||
' ' +
|
||
pad(now.getHours()) +
|
||
':' +
|
||
pad(now.getMinutes()) +
|
||
':' +
|
||
pad(now.getSeconds())
|
||
return {
|
||
solutionType: '',
|
||
fixedVersion: '',
|
||
resolvedAt: ts,
|
||
assigneeId: '',
|
||
remarkHtml: '<p><br></p>'
|
||
}
|
||
}
|
||
|
||
export default {
|
||
name: 'BugDetail',
|
||
components: { PageSection, BugStepsRichEditor },
|
||
data() {
|
||
return {
|
||
loading: false,
|
||
bugId: '',
|
||
feedMainTab: 'comments',
|
||
detail: {},
|
||
commentDialogVisible: false,
|
||
commentDialogKey: 0,
|
||
commentRemarkHtml: '',
|
||
commentSubmitting: false,
|
||
actionSaving: false,
|
||
deleteSaving: false,
|
||
reopenSaving: false,
|
||
resolvedProductName: '',
|
||
resolvedProjectName: '',
|
||
resolvedModuleName: '',
|
||
resolveDialogVisible: false,
|
||
resolveDialogKey: 0,
|
||
resolveDialogSaving: false,
|
||
resolveForm: getDefaultResolveForm(),
|
||
resolveFormRules: {
|
||
solutionType: [{ required: true, message: '请选择解决方案', trigger: 'change' }],
|
||
resolvedAt: [{ required: true, message: '请选择解决日期', trigger: 'change' }],
|
||
assigneeId: [{ required: true, message: '请选择当前指派', trigger: 'change' }]
|
||
},
|
||
resolveMemberOptions: [],
|
||
solutionOptions: [
|
||
{ label: '设计如此', value: 'by_design' },
|
||
{ label: '重复Bug', value: 'duplicate_bug' },
|
||
{ label: '外部原因', value: 'external_reason' },
|
||
{ label: '已解决', value: 'solution_resolved' },
|
||
{ label: '无法重现', value: 'cannot_reproduce' },
|
||
{ label: '延期处理', value: 'deferred' },
|
||
{ label: '不予解决', value: 'wont_fix' }
|
||
],
|
||
versionOptions: [
|
||
{ label: '主干', value: 'trunk' },
|
||
{ label: '当前迭代', value: 'current_sprint' },
|
||
{ label: '下一版本', value: 'next' }
|
||
]
|
||
}
|
||
},
|
||
computed: {
|
||
nextStatusTransition() {
|
||
return getBugStatusNextTransition(this.detail.status)
|
||
},
|
||
isResolvedStatus() {
|
||
return Number(this.detail.status) === BUG_STATUS_RESOLVED
|
||
},
|
||
isClosedStatus() {
|
||
return Number(this.detail.status) === BUG_STATUS_CLOSED
|
||
},
|
||
/** 已解决或已关闭时不可再点「解决」弹窗 */
|
||
cannotUseResolveButton() {
|
||
return this.isResolvedStatus || this.isClosedStatus
|
||
},
|
||
/** 已解决 / 已关闭 / 已拒绝 可重新打开为待处理 */
|
||
canReopenBug() {
|
||
const s = Number(this.detail.status)
|
||
return s === BUG_STATUS_RESOLVED || s === BUG_STATUS_CLOSED || s === BUG_STATUS_REJECTED
|
||
},
|
||
detailBugType() {
|
||
const d = this.detail || {}
|
||
return d.bug_type != null ? d.bug_type : d.bugType
|
||
},
|
||
reporterDisplay() {
|
||
const d = this.detail || {}
|
||
const name =
|
||
d.reporter_real_name ||
|
||
d.reporterRealName ||
|
||
d.reporter_name ||
|
||
d.reporterName ||
|
||
d.creator_real_name ||
|
||
d.creatorRealName ||
|
||
d.creator_name ||
|
||
d.creatorName ||
|
||
''
|
||
const id = d.reporter_id || d.reporterId || d.creator_id || d.creatorId || d.created_by || d.createdBy
|
||
if (name) return name
|
||
if (id !== undefined && id !== null && id !== '') return String(id)
|
||
return '-'
|
||
},
|
||
assigneeDisplay() {
|
||
const d = this.detail || {}
|
||
const name =
|
||
d.assignee_real_name ||
|
||
d.assigneeRealName ||
|
||
d.assignee_name ||
|
||
d.assigneeName ||
|
||
''
|
||
const id = d.assignee_id || d.assigneeId
|
||
if (name) return name
|
||
if (id !== undefined && id !== null && id !== '') return String(id)
|
||
return '-'
|
||
},
|
||
/** 解决操作中的「解决人」:仅展示,提交时 resolvedBy 取当前登录用户 id */
|
||
currentResolverDisplay() {
|
||
const u = this.$store.state.currentUser
|
||
if (!u) return '—'
|
||
return u.real_name || u.realName || u.username || (u.id != null ? String(u.id) : '—')
|
||
},
|
||
productDisplay() {
|
||
const d = this.detail || {}
|
||
const name = d.product_name || d.productName || this.resolvedProductName
|
||
const id = d.product_id || d.productId
|
||
if (name) return name
|
||
if (id !== undefined && id !== null && id !== '') return String(id)
|
||
return '-'
|
||
},
|
||
projectDisplay() {
|
||
const d = this.detail || {}
|
||
const name = d.project_name || d.projectName || this.resolvedProjectName
|
||
const id = d.project_id || d.projectId
|
||
if (name) return name
|
||
if (id !== undefined && id !== null && id !== '') return String(id)
|
||
return '-'
|
||
},
|
||
moduleDisplay() {
|
||
const d = this.detail || {}
|
||
const name = d.module_name || d.moduleName || this.resolvedModuleName
|
||
const id = d.module_id || d.moduleId
|
||
if (name) return name
|
||
if (id !== undefined && id !== null && id !== '') return String(id)
|
||
return '-'
|
||
},
|
||
commentList() {
|
||
const c = this.detail.comments
|
||
return Array.isArray(c) ? c : []
|
||
},
|
||
historyList() {
|
||
const h = this.detail.history
|
||
return Array.isArray(h) ? h : []
|
||
},
|
||
detailStepsRaw() {
|
||
const d = this.detail || {}
|
||
const candidates = [
|
||
d.steps,
|
||
d.reproduce_steps,
|
||
d.reproduceSteps,
|
||
d.reproduction_steps,
|
||
d.reproductionSteps
|
||
]
|
||
for (let i = 0; i < candidates.length; i++) {
|
||
const v = candidates[i]
|
||
if (v !== undefined && v !== null) return v
|
||
}
|
||
return ''
|
||
},
|
||
hasStepsContent() {
|
||
return String(this.detailStepsRaw).trim() !== ''
|
||
},
|
||
stepsDisplayHtml() {
|
||
const raw = this.detailStepsRaw
|
||
if (!String(raw).trim()) return ''
|
||
if (isStepsLikelyHtml(raw)) {
|
||
return rewriteImgSrcsInHtml(raw)
|
||
}
|
||
return formatBugStepsToHtml(raw)
|
||
},
|
||
effectiveBugId() {
|
||
const id = this.bugId != null && this.bugId !== '' ? String(this.bugId) : ''
|
||
return id
|
||
},
|
||
resolveHistoryPreviewList() {
|
||
const list = this.historyList || []
|
||
return list.slice(0, 20).map((h, idx) => ({
|
||
idx,
|
||
raw: h,
|
||
time: this.formatTime(h.created_time || h.createdTime),
|
||
fieldKey: normalizeBugHistoryFieldKey(h.field_name || h.fieldName),
|
||
oldVal: h.old_value !== undefined && h.old_value !== null ? h.old_value : h.oldValue,
|
||
newVal: h.new_value !== undefined && h.new_value !== null ? h.new_value : h.newValue,
|
||
op: this.historyOperatorLabel(h)
|
||
}))
|
||
}
|
||
},
|
||
watch: {
|
||
$route: {
|
||
handler() {
|
||
this.syncBugIdFromRoute()
|
||
if (this.bugId) {
|
||
this.loadDetail()
|
||
} else {
|
||
this.detail = {}
|
||
}
|
||
},
|
||
immediate: true
|
||
}
|
||
},
|
||
methods: {
|
||
formatHistoryFieldName: formatBugHistoryFieldName,
|
||
formatHistoryCellValue: formatBugHistoryCellValue,
|
||
historyNormKey(h) {
|
||
return normalizeBugHistoryFieldKey((h && (h.field_name || h.fieldName)) || '')
|
||
},
|
||
historyFieldUsesColoredTags(h) {
|
||
const k = this.historyNormKey(h)
|
||
return this.historyFieldKeyUsesColoredTags(k)
|
||
},
|
||
historyFieldKeyUsesColoredTags(fieldKey) {
|
||
const k = String(fieldKey || '').trim()
|
||
return k === 'status' || k === 'priority' || k === 'severity'
|
||
},
|
||
historyValueIsEmpty(val) {
|
||
return val === undefined || val === null || val === ''
|
||
},
|
||
historyTagEffect(val) {
|
||
return this.historyValueIsEmpty(val) ? 'plain' : 'dark'
|
||
},
|
||
historyTagType(fieldKey, raw) {
|
||
const k = String(fieldKey || '').trim()
|
||
if (this.historyValueIsEmpty(raw)) return 'info'
|
||
const n = Number(String(raw).trim())
|
||
if (Number.isNaN(n)) return undefined
|
||
if (k === 'status') return statusTagType(n)
|
||
if (k === 'priority') return priorityTagType(n)
|
||
if (k === 'severity') return severityTagType(n)
|
||
return 'info'
|
||
},
|
||
historyOperatorLabel(h) {
|
||
const d = h || {}
|
||
return d.operator_name || d.operatorName || d.operator_real_name || d.operatorRealName || d.operator_id || d.operatorId || '-'
|
||
},
|
||
syncBugIdFromRoute() {
|
||
const q = (this.$route && this.$route.query) || {}
|
||
if (q.bugId != null && q.bugId !== '') {
|
||
this.bugId = String(q.bugId)
|
||
return
|
||
}
|
||
if (q.id != null && q.id !== '') {
|
||
this.bugId = String(q.id)
|
||
return
|
||
}
|
||
this.bugId = ''
|
||
},
|
||
formatBugType,
|
||
formatSeverity,
|
||
formatPriority,
|
||
formatStatus,
|
||
statusBadgeClass,
|
||
severityTagType,
|
||
priorityTagType,
|
||
formatTime(v) {
|
||
if (!v) return '-'
|
||
return String(v).replace('T', ' ').slice(0, 19)
|
||
},
|
||
findModuleNameInTree(nodes, targetId) {
|
||
const idStr = String(targetId)
|
||
const walk = (list, pfx) => {
|
||
for (let i = 0; i < (list || []).length; i++) {
|
||
const n = list[i]
|
||
const name = pfx ? `${pfx} / ${n.name}` : n.name
|
||
if (String(n.id) === idStr) return name
|
||
const ch = n.children || n.child_list || n.childList || []
|
||
if (ch.length) {
|
||
const hit = walk(ch, name)
|
||
if (hit) return hit
|
||
}
|
||
}
|
||
return ''
|
||
}
|
||
return walk(nodes || [], '')
|
||
},
|
||
loadScopeNames() {
|
||
const d = this.detail || {}
|
||
const productId = d.product_id || d.productId
|
||
const projectId = d.project_id || d.projectId
|
||
const moduleId = d.module_id || d.moduleId
|
||
this.resolvedProductName = ''
|
||
this.resolvedProjectName = ''
|
||
this.resolvedModuleName = ''
|
||
if (d.product_name || d.productName) {
|
||
this.resolvedProductName = d.product_name || d.productName
|
||
}
|
||
if (d.project_name || d.projectName) {
|
||
this.resolvedProjectName = d.project_name || d.projectName
|
||
}
|
||
if (d.module_name || d.moduleName) {
|
||
this.resolvedModuleName = d.module_name || d.moduleName
|
||
}
|
||
const tasks = []
|
||
if (!this.resolvedProductName && productId) {
|
||
tasks.push(
|
||
getProductList({ pageNo: 1, pageSize: 1000, status: 1 }).then(res => {
|
||
const data = (res && res.data) || res || {}
|
||
const items = data.items || data.list || data.data || []
|
||
const p = (items || []).find(x => String(x.id) === String(productId))
|
||
if (p) this.resolvedProductName = p.name || ''
|
||
}).catch(() => {})
|
||
)
|
||
}
|
||
if (!this.resolvedProjectName && projectId) {
|
||
tasks.push(
|
||
getProjectDetail(projectId).then(res => {
|
||
const data = (res && res.data) || res || {}
|
||
this.resolvedProjectName = data.name || ''
|
||
}).catch(() => {})
|
||
)
|
||
}
|
||
if (!this.resolvedModuleName && projectId && moduleId) {
|
||
tasks.push(
|
||
getModuleTree({ projectId, pageNo: 1, pageSize: 1000 }).then(res => {
|
||
const data = (res && res.data) || res || {}
|
||
const tree = data.list || data.items || []
|
||
this.resolvedModuleName = this.findModuleNameInTree(tree, moduleId) || ''
|
||
}).catch(() => {})
|
||
)
|
||
}
|
||
return Promise.all(tasks)
|
||
},
|
||
loadDetail() {
|
||
if (!this.bugId) return
|
||
this.loading = true
|
||
this.resolvedProductName = ''
|
||
this.resolvedProjectName = ''
|
||
this.resolvedModuleName = ''
|
||
getBugDetail(this.bugId).then(res => {
|
||
this.detail = (res && res.data) || res || {}
|
||
return this.loadScopeNames()
|
||
}).catch(() => { this.detail = {} }).finally(() => { this.loading = false })
|
||
},
|
||
applyReopen() {
|
||
if (!this.bugId) return
|
||
if (!this.canReopenBug) {
|
||
this.$message.info('当前状态无需重新打开')
|
||
return
|
||
}
|
||
this.reopenSaving = true
|
||
const oldStatus = this.detail.status
|
||
updateBug({
|
||
bugId: Number(this.bugId),
|
||
id: Number(this.bugId),
|
||
status: BUG_STATUS_PENDING
|
||
})
|
||
.then(() => {
|
||
recordBugHistory(this.$store, {
|
||
bugId: this.bugId,
|
||
fieldName: 'status',
|
||
oldValue: oldStatus,
|
||
newValue: BUG_STATUS_PENDING
|
||
})
|
||
this.$message.success('已重新打开为待处理')
|
||
this.loadDetail()
|
||
})
|
||
.finally(() => {
|
||
this.reopenSaving = false
|
||
})
|
||
},
|
||
applyNextStatus() {
|
||
const t = this.nextStatusTransition
|
||
if (!this.bugId || !t) return
|
||
this.actionSaving = true
|
||
const oldStatus = this.detail.status
|
||
updateBug({
|
||
bugId: Number(this.bugId),
|
||
id: Number(this.bugId),
|
||
status: t.nextStatus
|
||
})
|
||
.then(() => {
|
||
recordBugHistory(this.$store, {
|
||
bugId: this.bugId,
|
||
fieldName: 'status',
|
||
oldValue: oldStatus,
|
||
newValue: t.nextStatus
|
||
})
|
||
this.$message.success('状态已更新')
|
||
this.loadDetail()
|
||
})
|
||
.finally(() => {
|
||
this.actionSaving = false
|
||
})
|
||
},
|
||
mapMemberRows(list) {
|
||
const arr = Array.isArray(list) ? list : []
|
||
return arr
|
||
.map(item => ({
|
||
id: item.user_id || item.userId || item.id,
|
||
name:
|
||
item.real_name ||
|
||
item.realName ||
|
||
item.username ||
|
||
item.name ||
|
||
item.user_name ||
|
||
String(item.user_id || item.id)
|
||
}))
|
||
.filter(u => u.id !== undefined && u.id !== null)
|
||
},
|
||
loadResolveMembers() {
|
||
const pid = this.detail.project_id || this.detail.projectId
|
||
if (!pid) {
|
||
this.resolveMemberOptions = []
|
||
return Promise.resolve()
|
||
}
|
||
return getProjectMembers(pid, { pageNo: 1, pageSize: 500 })
|
||
.then(res => {
|
||
const data = (res && res.data) || res || {}
|
||
const raw = data.items || data.list || data.data || []
|
||
this.resolveMemberOptions = this.mapMemberRows(Array.isArray(raw) ? raw : [])
|
||
})
|
||
.catch(() => {
|
||
this.resolveMemberOptions = []
|
||
})
|
||
},
|
||
onResolveAssigneeFocus() {
|
||
this.loadResolveMembers()
|
||
},
|
||
openResolveDialog() {
|
||
if (!this.bugId) return
|
||
if (this.cannotUseResolveButton) {
|
||
if (this.isClosedStatus) {
|
||
this.$message.info('已关闭的 Bug 请使用「重新打开」')
|
||
} else {
|
||
this.$message.info('当前已是已解决')
|
||
}
|
||
return
|
||
}
|
||
this.resolveForm = getDefaultResolveForm()
|
||
this.resolveForm.assigneeId = this.detail.assignee_id || this.detail.assigneeId || ''
|
||
this.resolveDialogKey += 1
|
||
this.resolveDialogVisible = true
|
||
this.$nextTick(() => {
|
||
this.loadResolveMembers()
|
||
if (this.$refs.resolveFormRef) {
|
||
this.$refs.resolveFormRef.clearValidate()
|
||
}
|
||
})
|
||
},
|
||
resetResolveForm() {
|
||
this.resolveForm = getDefaultResolveForm()
|
||
this.resolveMemberOptions = []
|
||
if (this.$refs.resolveFormRef) {
|
||
this.$refs.resolveFormRef.resetFields()
|
||
}
|
||
},
|
||
/** 解决弹窗备注:空编辑器不传;保留含截图的 HTML(img 为 /bug/upload 地址) */
|
||
resolveRemarkHtmlForApi(html) {
|
||
const h = (html || '').trim()
|
||
if (!h || h === '<p><br></p>' || h === '<p><br/></p>') return ''
|
||
return h
|
||
},
|
||
submitResolveDialog() {
|
||
if (!this.bugId) return
|
||
this.$refs.resolveFormRef.validate(valid => {
|
||
if (!valid) return
|
||
const user = this.$store.state.currentUser
|
||
const uid = user && user.id
|
||
if (uid === undefined || uid === null || uid === '') {
|
||
this.$message.warning('未获取到当前登录用户')
|
||
return
|
||
}
|
||
this.resolveDialogSaving = true
|
||
const oldStatus = this.detail.status
|
||
const oldSolution =
|
||
this.detail.solution ||
|
||
this.detail.solution_type ||
|
||
this.detail.solutionType ||
|
||
''
|
||
const resolvedBy = Number(uid)
|
||
const assigneeId = Number(this.resolveForm.assigneeId)
|
||
const clean = {
|
||
bugId: Number(this.bugId),
|
||
id: Number(this.bugId),
|
||
status: BUG_STATUS_RESOLVED,
|
||
solution: this.resolveForm.solutionType,
|
||
user_id: Number(uid),
|
||
assigneeId,
|
||
resolvedBy,
|
||
comment: this.resolveRemarkHtmlForApi(this.resolveForm.remarkHtml)
|
||
}
|
||
if (this.resolveForm.fixedVersion !== '' && this.resolveForm.fixedVersion != null) {
|
||
clean.resolveVersion = this.resolveForm.fixedVersion
|
||
}
|
||
const oldResolvedBy =
|
||
this.detail.resolved_by != null && this.detail.resolved_by !== ''
|
||
? this.detail.resolved_by
|
||
: this.detail.resolvedBy != null && this.detail.resolvedBy !== ''
|
||
? this.detail.resolvedBy
|
||
: ''
|
||
const oldAssigneeId =
|
||
this.detail.assignee_id != null && this.detail.assignee_id !== ''
|
||
? this.detail.assignee_id
|
||
: this.detail.assigneeId != null && this.detail.assigneeId !== ''
|
||
? this.detail.assigneeId
|
||
: ''
|
||
updateBug(clean)
|
||
.then(() => {
|
||
recordBugHistory(this.$store, {
|
||
bugId: this.bugId,
|
||
fieldName: 'status',
|
||
oldValue: oldStatus,
|
||
newValue: BUG_STATUS_RESOLVED
|
||
})
|
||
recordBugHistory(this.$store, {
|
||
bugId: this.bugId,
|
||
fieldName: 'solution',
|
||
oldValue: oldSolution,
|
||
newValue: this.resolveForm.solutionType
|
||
})
|
||
if (String(oldAssigneeId) !== String(assigneeId)) {
|
||
recordBugHistory(this.$store, {
|
||
bugId: this.bugId,
|
||
fieldName: 'assignee_id',
|
||
oldValue: oldAssigneeId,
|
||
newValue: assigneeId
|
||
})
|
||
}
|
||
if (String(oldResolvedBy) !== String(resolvedBy)) {
|
||
recordBugHistory(this.$store, {
|
||
bugId: this.bugId,
|
||
fieldName: 'resolved_by',
|
||
oldValue: oldResolvedBy,
|
||
newValue: resolvedBy
|
||
})
|
||
}
|
||
if (this.resolveForm.fixedVersion !== '' && this.resolveForm.fixedVersion != null) {
|
||
recordBugHistory(this.$store, {
|
||
bugId: this.bugId,
|
||
fieldName: 'resolve_version',
|
||
oldValue: '',
|
||
newValue: this.resolveForm.fixedVersion
|
||
})
|
||
}
|
||
const remark = this.resolveRemarkHtmlForApi(this.resolveForm.remarkHtml)
|
||
if (remark) {
|
||
recordBugHistory(this.$store, {
|
||
bugId: this.bugId,
|
||
fieldName: 'resolve_comment',
|
||
oldValue: '',
|
||
newValue: remark.length > 2000 ? remark.slice(0, 2000) : remark
|
||
})
|
||
}
|
||
this.$message.success('已保存为已解决')
|
||
this.resolveDialogVisible = false
|
||
this.loadDetail()
|
||
})
|
||
.catch(() => {})
|
||
.finally(() => {
|
||
this.resolveDialogSaving = false
|
||
})
|
||
})
|
||
},
|
||
confirmDelete() {
|
||
if (!this.bugId) return
|
||
this.$confirm('确认删除该 Bug?删除后不可恢复。', '删除确认', {
|
||
type: 'warning',
|
||
confirmButtonText: '删除',
|
||
cancelButtonText: '取消'
|
||
})
|
||
.then(() => {
|
||
this.deleteSaving = true
|
||
const bid = Number(this.bugId)
|
||
return recordBugHistory(this.$store, {
|
||
bugId: bid,
|
||
fieldName: 'delete',
|
||
oldValue: '0',
|
||
newValue: '1'
|
||
}).then(() => deleteBug({ bugId: bid, id: bid }))
|
||
})
|
||
.then(() => {
|
||
this.$message.success('已删除')
|
||
this.$router.push({ path: '/bug/list' })
|
||
})
|
||
.catch(() => {})
|
||
.finally(() => {
|
||
this.deleteSaving = false
|
||
})
|
||
},
|
||
openCommentDialog() {
|
||
this.commentRemarkHtml = ''
|
||
this.commentDialogKey += 1
|
||
this.commentDialogVisible = true
|
||
},
|
||
resetCommentDialog() {
|
||
this.commentRemarkHtml = ''
|
||
},
|
||
submitCommentFromDialog() {
|
||
const content = this.resolveRemarkHtmlForApi(this.commentRemarkHtml)
|
||
if (!content) {
|
||
this.$message.warning('请输入备注内容')
|
||
return
|
||
}
|
||
const user = this.$store.state.currentUser
|
||
const uid = user && user.id
|
||
if (uid === undefined || uid === null || uid === '') {
|
||
this.$message.warning('未获取到当前登录用户')
|
||
return
|
||
}
|
||
this.commentSubmitting = true
|
||
addBugComment({
|
||
bugId: Number(this.bugId),
|
||
id: Number(this.bugId),
|
||
content,
|
||
user_id: Number(uid)
|
||
}).then(() => {
|
||
recordBugHistory(this.$store, {
|
||
bugId: Number(this.bugId),
|
||
fieldName: 'comment',
|
||
oldValue: '',
|
||
newValue: content.length > 2000 ? content.slice(0, 2000) : content
|
||
})
|
||
this.$message.success('评论已提交')
|
||
this.commentDialogVisible = false
|
||
this.resetCommentDialog()
|
||
this.loadDetail()
|
||
}).finally(() => {
|
||
this.commentSubmitting = false
|
||
})
|
||
},
|
||
goBack() {
|
||
this.$router.push({ path: '/bug/list' })
|
||
},
|
||
goEdit() {
|
||
this.$router.push({ path: '/bug/edit', query: { bugId: this.bugId } })
|
||
}
|
||
},
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page-wrap {
|
||
padding: 20px;
|
||
}
|
||
.page-wrap--bug-detail {
|
||
background: #f0f2f5;
|
||
margin: -20px;
|
||
padding: 20px;
|
||
min-height: calc(100vh - 60px);
|
||
}
|
||
.page-wrap--fixed-actions {
|
||
padding-bottom: 88px;
|
||
}
|
||
.page-wrap--bug-detail >>> .page-section.el-card {
|
||
background: transparent;
|
||
border: none;
|
||
box-shadow: none;
|
||
}
|
||
.page-wrap--bug-detail >>> .page-section > .el-card__header {
|
||
border-bottom: none;
|
||
background: transparent;
|
||
padding: 0 0 16px;
|
||
}
|
||
.page-wrap--bug-detail >>> .page-section > .el-card__body {
|
||
padding: 0;
|
||
background: transparent;
|
||
}
|
||
.detail-layout {
|
||
min-height: 200px;
|
||
}
|
||
.detail-top-loading {
|
||
min-height: 120px;
|
||
}
|
||
.detail-card {
|
||
background: #fff;
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
}
|
||
.main-card {
|
||
position: relative;
|
||
}
|
||
.main-card-head {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
}
|
||
.main-card-head-left {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
.status-corner {
|
||
flex-shrink: 0;
|
||
padding: 5px 14px;
|
||
border-radius: 4px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
line-height: 1.4;
|
||
}
|
||
.bug-status-new {
|
||
background: #ecf5ff;
|
||
color: #409eff;
|
||
}
|
||
.bug-status-pending {
|
||
background: #fdf6ec;
|
||
color: #e6a23c;
|
||
}
|
||
.bug-status-progress {
|
||
background: #e8f4ff;
|
||
color: #1989fa;
|
||
border: 1px solid #b3d8ff;
|
||
}
|
||
.bug-status-resolved {
|
||
background: #f0f9eb;
|
||
color: #67c23a;
|
||
}
|
||
.bug-status-closed {
|
||
background: #f4f4f5;
|
||
color: #909399;
|
||
}
|
||
.bug-status-rejected {
|
||
background: #fef0f0;
|
||
color: #f56c6c;
|
||
}
|
||
.bug-status-unknown {
|
||
background: #f4f4f5;
|
||
color: #606266;
|
||
}
|
||
.mt {
|
||
margin-top: 16px;
|
||
}
|
||
.mt-sm {
|
||
margin-top: 10px;
|
||
}
|
||
.bug-key {
|
||
font-weight: 600;
|
||
color: #409eff;
|
||
margin-right: 4px;
|
||
}
|
||
.bug-title {
|
||
margin: 12px 0 18px;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
}
|
||
.meta-rows {
|
||
margin-bottom: 14px;
|
||
}
|
||
.meta-rows-last {
|
||
margin-bottom: 0;
|
||
}
|
||
.meta-field {
|
||
min-height: 56px;
|
||
}
|
||
.meta-label {
|
||
font-size: 13px;
|
||
color: #909399;
|
||
margin-bottom: 8px;
|
||
line-height: 1.5;
|
||
font-weight: 500;
|
||
}
|
||
.meta-value {
|
||
font-size: 15px;
|
||
color: #303133;
|
||
line-height: 1.55;
|
||
word-break: break-word;
|
||
}
|
||
.steps-card {
|
||
min-height: 72px;
|
||
}
|
||
.steps-empty {
|
||
color: #909399;
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
}
|
||
.block-title {
|
||
font-weight: 600;
|
||
color: #606266;
|
||
margin: 12px 0 8px;
|
||
}
|
||
.steps-display {
|
||
line-height: 1.6;
|
||
color: #303133;
|
||
word-break: break-word;
|
||
}
|
||
.steps-display >>> .bug-step-img,
|
||
.steps-display >>> img {
|
||
display: block;
|
||
max-width: 100%;
|
||
margin: 10px 0;
|
||
border-radius: 4px;
|
||
border: 1px solid #ebeef5;
|
||
}
|
||
.steps-display >>> p {
|
||
margin: 0 0 8px;
|
||
}
|
||
.comment-item {
|
||
border-bottom: 1px solid #ebeef5;
|
||
padding: 10px 0;
|
||
}
|
||
.comment-meta {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
}
|
||
.comment-body {
|
||
margin-top: 6px;
|
||
color: #303133;
|
||
line-height: 1.6;
|
||
word-break: break-word;
|
||
}
|
||
.comment-body >>> img {
|
||
display: block;
|
||
max-width: 100%;
|
||
height: auto;
|
||
margin: 8px 0;
|
||
border-radius: 4px;
|
||
border: 1px solid #ebeef5;
|
||
}
|
||
.comment-body >>> p {
|
||
margin: 0 0 6px;
|
||
}
|
||
.empty-hint {
|
||
color: #909399;
|
||
font-size: 13px;
|
||
}
|
||
.feed-tab-card {
|
||
padding-top: 18px;
|
||
}
|
||
.feed-pill-tabs {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
}
|
||
.feed-pill-tab {
|
||
border: none;
|
||
border-radius: 18px;
|
||
padding: 6px 18px;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
cursor: pointer;
|
||
background: #f4f4f5;
|
||
color: #606266;
|
||
outline: none;
|
||
transition: background 0.2s, color 0.2s;
|
||
}
|
||
.feed-pill-tab:hover:not(.is-active) {
|
||
color: #303133;
|
||
background: #ebeef5;
|
||
}
|
||
.feed-pill-tab.is-active {
|
||
background: #ecf5ff;
|
||
color: #409eff;
|
||
font-weight: 500;
|
||
}
|
||
.feed-tab-divider {
|
||
height: 1px;
|
||
background: #ebeef5;
|
||
margin: 12px 0 8px;
|
||
}
|
||
.feed-tab-panel {
|
||
min-height: 48px;
|
||
}
|
||
.comment-tab-toolbar {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-bottom: 10px;
|
||
}
|
||
.comment-add-remark-btn {
|
||
flex-shrink: 0;
|
||
}
|
||
.history-field {
|
||
font-weight: 600;
|
||
margin-right: 8px;
|
||
}
|
||
.history-arrow {
|
||
color: #606266;
|
||
}
|
||
.history-op {
|
||
margin-left: 12px;
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
.history-line {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: baseline;
|
||
gap: 4px 8px;
|
||
}
|
||
.history-tag-pair {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
vertical-align: middle;
|
||
}
|
||
.history-arrow-symbol {
|
||
color: #909399;
|
||
font-weight: 500;
|
||
user-select: none;
|
||
}
|
||
.history-arrow-symbol--text {
|
||
margin: 0 4px;
|
||
}
|
||
.history-arrow--inline {
|
||
margin-right: 4px;
|
||
}
|
||
.history-enum-tag {
|
||
vertical-align: middle;
|
||
}
|
||
.bug-action-dock {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 200;
|
||
padding: 12px 20px calc(14px + env(safe-area-inset-bottom, 0px));
|
||
background: transparent;
|
||
border: none;
|
||
box-shadow: none;
|
||
pointer-events: none;
|
||
}
|
||
.bug-action-dock-inner {
|
||
width: 100%;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
pointer-events: none;
|
||
}
|
||
.bug-action-strip {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
flex-wrap: wrap;
|
||
gap: 10px 12px;
|
||
padding: 12px 18px;
|
||
background: #fff;
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||
pointer-events: auto;
|
||
}
|
||
.bug-action-strip-divider {
|
||
width: 1px;
|
||
height: 18px;
|
||
background: #dcdfe6;
|
||
flex-shrink: 0;
|
||
}
|
||
</style>
|
||
|
||
<style>
|
||
/* el-dialog 挂载到 body,需非 scoped */
|
||
.bug-resolve-dialog .el-dialog__header {
|
||
padding: 16px 36px 10px;
|
||
border-bottom: 1px solid #ebeef5;
|
||
}
|
||
.bug-resolve-dialog .el-dialog__body {
|
||
padding: 20px 40px 16px;
|
||
}
|
||
.bug-resolve-dialog .resolve-dialog-form {
|
||
max-width: 640px;
|
||
margin: 0 auto;
|
||
}
|
||
.bug-resolve-dialog .resolve-dialog-form .el-form-item {
|
||
margin-bottom: 18px;
|
||
}
|
||
.bug-resolve-dialog .resolve-dialog-form .el-form-item__label {
|
||
padding-right: 16px;
|
||
}
|
||
.bug-resolve-dialog .resolve-field-select .el-input__inner,
|
||
.bug-resolve-dialog .resolve-dialog-form .el-date-editor .el-input__inner,
|
||
.bug-resolve-dialog .resolve-dialog-form .el-select:not(.resolve-field-select) .el-input__inner {
|
||
border-radius: 6px;
|
||
}
|
||
.bug-resolve-dialog .el-dialog__headerbtn {
|
||
top: 14px;
|
||
}
|
||
.bug-resolve-dialog .resolve-dialog-header {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 12px;
|
||
padding-right: 28px;
|
||
}
|
||
.bug-resolve-dialog .resolve-dialog-id {
|
||
flex-shrink: 0;
|
||
background: #f0f2f5;
|
||
color: #909399;
|
||
padding: 2px 10px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
.bug-resolve-dialog .resolve-dialog-title-text {
|
||
flex: 1;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
line-height: 1.5;
|
||
text-align: left;
|
||
}
|
||
.bug-resolve-dialog .resolve-resolver-readonly {
|
||
color: #303133;
|
||
font-weight: 500;
|
||
}
|
||
.bug-resolve-dialog .resolve-resolver-hint {
|
||
margin-left: 8px;
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
.bug-resolve-dialog .resolve-remark-item .el-form-item__content {
|
||
line-height: 1.4;
|
||
}
|
||
.bug-resolve-dialog .resolve-dialog-footer {
|
||
text-align: center;
|
||
padding: 8px 0 4px;
|
||
max-width: 640px;
|
||
margin: 0 auto;
|
||
}
|
||
.bug-resolve-dialog .resolve-history-block {
|
||
margin-top: 16px;
|
||
margin-left: auto;
|
||
margin-right: auto;
|
||
max-width: 640px;
|
||
padding-top: 14px;
|
||
border-top: 1px solid #ebeef5;
|
||
}
|
||
.bug-resolve-dialog .resolve-history-title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #606266;
|
||
margin-bottom: 10px;
|
||
}
|
||
.bug-resolve-dialog .resolve-history-list {
|
||
margin: 0;
|
||
padding-left: 20px;
|
||
color: #606266;
|
||
font-size: 13px;
|
||
line-height: 1.7;
|
||
}
|
||
.bug-resolve-dialog .resolve-history-list li {
|
||
margin-bottom: 6px;
|
||
}
|
||
.bug-resolve-dialog .resolve-history-time {
|
||
color: #909399;
|
||
font-size: 12px;
|
||
}
|
||
.bug-resolve-dialog .resolve-history-op {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
.bug-resolve-dialog .resolve-history-empty {
|
||
font-size: 13px;
|
||
color: #909399;
|
||
}
|
||
/* 解决方案下拉面挂载在 body */
|
||
.bug-resolve-solution-dropdown.el-select-dropdown {
|
||
border-radius: 6px;
|
||
}
|
||
.bug-resolve-solution-dropdown .el-select-dropdown__item {
|
||
padding: 0 20px;
|
||
line-height: 36px;
|
||
}
|
||
.bug-resolve-solution-dropdown .el-select-dropdown__list {
|
||
padding: 6px 0;
|
||
}
|
||
|
||
.bug-comment-dialog .el-dialog__body {
|
||
padding: 12px 20px 8px;
|
||
}
|
||
.bug-comment-dialog .bug-comment-dialog-footer {
|
||
text-align: center;
|
||
}
|
||
.bug-comment-dialog .bug-comment-dialog-footer .el-button + .el-button {
|
||
margin-left: 12px;
|
||
}
|
||
</style>
|