|
|
@@ -48,7 +48,7 @@
|
|
|
</div>
|
|
|
|
|
|
<v-alert type="info" outlined class="mb-5">
|
|
|
- 质量检查读取设备台账和已发布语义代码,不改写来源数据。违规修复、责任派发和复验闭环属于 WP08。
|
|
|
+ 质量检查不改写来源数据;整改问题保留违规证据快照,并通过分派、整改、独立复核、关闭和重开形成审计闭环。
|
|
|
</v-alert>
|
|
|
|
|
|
<v-row class="mb-1">
|
|
|
@@ -135,6 +135,146 @@
|
|
|
</v-data-table>
|
|
|
</v-card>
|
|
|
|
|
|
+ <v-card outlined class="mb-5">
|
|
|
+ <v-card-title class="flex-wrap">
|
|
|
+ 质量问题整改闭环
|
|
|
+ <v-spacer />
|
|
|
+ <v-chip small outlined class="mr-2">
|
|
|
+ 未关闭 {{ issueStatistics.open || 0 }}
|
|
|
+ </v-chip>
|
|
|
+ <v-chip small outlined color="error" class="mr-2">
|
|
|
+ 逾期 {{ issueStatistics.overdue || 0 }}
|
|
|
+ </v-chip>
|
|
|
+ <v-chip small outlined color="warning">
|
|
|
+ 复发组 {{ issueStatistics.recurrent || 0 }} ·
|
|
|
+ 复发率 {{ formatIssueRate(issueStatistics.recurrence_rate) }}
|
|
|
+ </v-chip>
|
|
|
+ </v-card-title>
|
|
|
+ <v-divider />
|
|
|
+ <div class="d-flex flex-wrap align-center pa-4">
|
|
|
+ <v-select
|
|
|
+ v-model="issueStatusFilter"
|
|
|
+ :items="issueStatusOptions"
|
|
|
+ item-text="text"
|
|
|
+ item-value="value"
|
|
|
+ label="问题状态"
|
|
|
+ clearable
|
|
|
+ dense
|
|
|
+ outlined
|
|
|
+ hide-details
|
|
|
+ class="issue-filter mr-3"
|
|
|
+ @change="reloadIssues"
|
|
|
+ />
|
|
|
+ <v-checkbox
|
|
|
+ v-model="overdueOnly"
|
|
|
+ label="只看逾期"
|
|
|
+ dense
|
|
|
+ hide-details
|
|
|
+ class="mt-0"
|
|
|
+ @change="reloadIssues"
|
|
|
+ />
|
|
|
+ <v-spacer />
|
|
|
+ <v-btn text color="primary" :loading="loadingIssues" @click="loadIssues">
|
|
|
+ 刷新
|
|
|
+ </v-btn>
|
|
|
+ </div>
|
|
|
+ <v-data-table
|
|
|
+ :headers="issueHeaders"
|
|
|
+ :items="issues"
|
|
|
+ :loading="loadingIssues"
|
|
|
+ :items-per-page="issuePageSize"
|
|
|
+ hide-default-footer
|
|
|
+ >
|
|
|
+ <template v-slot:[`item.issue_code`]="{ item }">
|
|
|
+ <strong>{{ item.issue_code }}</strong>
|
|
|
+ <div class="text-caption text--secondary">
|
|
|
+ {{ ruleLabel(item.rule_code) }}
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template v-slot:[`item.status`]="{ item }">
|
|
|
+ <v-chip small outlined :color="issueStatusColor(item.status)">
|
|
|
+ {{ issueStatusLabel(item.status) }}
|
|
|
+ </v-chip>
|
|
|
+ </template>
|
|
|
+ <template v-slot:[`item.priority`]="{ item }">
|
|
|
+ {{ priorityLabel(item.priority) }}
|
|
|
+ </template>
|
|
|
+ <template v-slot:[`item.recurrence`]="{ item }">
|
|
|
+ <span :class="{ 'warning--text': item.occurrence_number > 1 }">
|
|
|
+ {{ recurrenceLabel(item.occurrence_number) }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ <template v-slot:[`item.due_at`]="{ item }">
|
|
|
+ <span :class="{ 'error--text font-weight-bold': dueState(item) === 'overdue' }">
|
|
|
+ {{ displayTime(item.due_at) }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ <template v-slot:[`item.actions`]="{ item }">
|
|
|
+ <v-btn text small color="primary" @click="openIssue(item)">详情</v-btn>
|
|
|
+ <v-btn
|
|
|
+ v-if="canEditIssueRegister && item.status !== 'closed'"
|
|
|
+ text
|
|
|
+ small
|
|
|
+ @click="openAssign(item)"
|
|
|
+ >
|
|
|
+ 分派
|
|
|
+ </v-btn>
|
|
|
+ <v-btn
|
|
|
+ v-if="canEditIssueRegister && item.status === 'assigned'"
|
|
|
+ text
|
|
|
+ small
|
|
|
+ @click="startIssue(item)"
|
|
|
+ >
|
|
|
+ 开始
|
|
|
+ </v-btn>
|
|
|
+ <v-btn
|
|
|
+ v-if="canEditIssueRegister && ['assigned', 'in_progress'].includes(item.status)"
|
|
|
+ text
|
|
|
+ small
|
|
|
+ @click="openSubmit(item)"
|
|
|
+ >
|
|
|
+ 提交
|
|
|
+ </v-btn>
|
|
|
+ <v-btn
|
|
|
+ v-if="canReviewIssueRegister && item.status === 'pending_review'"
|
|
|
+ text
|
|
|
+ small
|
|
|
+ color="success"
|
|
|
+ @click="openReview(item)"
|
|
|
+ >
|
|
|
+ 复核
|
|
|
+ </v-btn>
|
|
|
+ <v-btn
|
|
|
+ v-if="canEditIssueRegister && item.status === 'closed'"
|
|
|
+ text
|
|
|
+ small
|
|
|
+ color="warning"
|
|
|
+ @click="openReopen(item)"
|
|
|
+ >
|
|
|
+ 重开
|
|
|
+ </v-btn>
|
|
|
+ </template>
|
|
|
+ <template v-slot:no-data>
|
|
|
+ <div class="py-8 text--secondary">
|
|
|
+ 暂无整改问题;可在质量检查的违规样本中选择并创建。
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </v-data-table>
|
|
|
+ <v-divider />
|
|
|
+ <div class="d-flex align-center pa-4">
|
|
|
+ <span class="text-caption text--secondary">
|
|
|
+ 共 {{ issueTotal }} 个问题,已关闭 {{ issueStatistics.closed || 0 }}
|
|
|
+ </span>
|
|
|
+ <v-spacer />
|
|
|
+ <v-pagination
|
|
|
+ v-model="issuePage"
|
|
|
+ :length="issuePageCount"
|
|
|
+ :total-visible="7"
|
|
|
+ @input="loadIssues"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </v-card>
|
|
|
+
|
|
|
<v-card outlined class="mb-5">
|
|
|
<v-card-title>质量检查记录</v-card-title>
|
|
|
<v-divider />
|
|
|
@@ -233,8 +373,11 @@
|
|
|
<v-tabs-items v-model="detailTab">
|
|
|
<v-tab-item>
|
|
|
<v-data-table
|
|
|
+ v-model="selectedViolations"
|
|
|
:headers="violationHeaders"
|
|
|
:items="violations"
|
|
|
+ item-key="uid"
|
|
|
+ show-select
|
|
|
:items-per-page="20"
|
|
|
hide-default-footer
|
|
|
>
|
|
|
@@ -256,6 +399,16 @@
|
|
|
<div class="text-caption text--secondary mt-2">
|
|
|
当前展示 {{ violations.length }} / {{ violationTotal }} 条;每条规则最多保留 100 条样本,30 天后清理。
|
|
|
</div>
|
|
|
+ <v-btn
|
|
|
+ v-if="canEditIssueRegister"
|
|
|
+ class="mt-3"
|
|
|
+ color="primary"
|
|
|
+ outlined
|
|
|
+ :disabled="selectedViolations.length === 0"
|
|
|
+ @click="issueImportDialog = true"
|
|
|
+ >
|
|
|
+ 创建整改问题({{ selectedViolations.length }})
|
|
|
+ </v-btn>
|
|
|
</v-tab-item>
|
|
|
<v-tab-item>
|
|
|
<v-data-table
|
|
|
@@ -284,6 +437,261 @@
|
|
|
</v-card>
|
|
|
</v-dialog>
|
|
|
|
|
|
+ <v-dialog v-model="issueImportDialog" max-width="620">
|
|
|
+ <v-card>
|
|
|
+ <v-card-title>创建质量整改问题</v-card-title>
|
|
|
+ <v-card-text>
|
|
|
+ <v-alert dense outlined type="info">
|
|
|
+ 将为 {{ selectedViolations.length }} 条违规保存证据快照;未关闭的同类问题不会重复创建。
|
|
|
+ </v-alert>
|
|
|
+ <v-select
|
|
|
+ v-model="issueForm.priority"
|
|
|
+ :items="priorityOptions"
|
|
|
+ item-text="text"
|
|
|
+ item-value="value"
|
|
|
+ label="优先级"
|
|
|
+ outlined
|
|
|
+ dense
|
|
|
+ />
|
|
|
+ <v-text-field
|
|
|
+ v-model="issueForm.dueAt"
|
|
|
+ type="datetime-local"
|
|
|
+ label="整改期限(可选)"
|
|
|
+ outlined
|
|
|
+ dense
|
|
|
+ />
|
|
|
+ </v-card-text>
|
|
|
+ <v-card-actions>
|
|
|
+ <v-spacer />
|
|
|
+ <v-btn text @click="issueImportDialog = false">取消</v-btn>
|
|
|
+ <v-btn color="primary" :loading="savingIssue" @click="createIssues">
|
|
|
+ 创建问题
|
|
|
+ </v-btn>
|
|
|
+ </v-card-actions>
|
|
|
+ </v-card>
|
|
|
+ </v-dialog>
|
|
|
+
|
|
|
+ <v-dialog v-model="issueDetailDialog" max-width="900" scrollable>
|
|
|
+ <v-card>
|
|
|
+ <v-card-title>
|
|
|
+ 整改问题详情
|
|
|
+ <v-spacer />
|
|
|
+ <v-btn icon @click="issueDetailDialog = false">
|
|
|
+ <v-icon>mdi-close</v-icon>
|
|
|
+ </v-btn>
|
|
|
+ </v-card-title>
|
|
|
+ <v-divider />
|
|
|
+ <v-card-text class="pt-5">
|
|
|
+ <v-progress-linear v-if="issueDetailLoading" indeterminate />
|
|
|
+ <template v-else-if="selectedIssue">
|
|
|
+ <v-row>
|
|
|
+ <v-col cols="12" sm="4">
|
|
|
+ <div class="field-label">问题编号 / 状态</div>
|
|
|
+ <div>
|
|
|
+ <strong>{{ selectedIssue.issue_code }}</strong>
|
|
|
+ · {{ issueStatusLabel(selectedIssue.status) }}
|
|
|
+ </div>
|
|
|
+ </v-col>
|
|
|
+ <v-col cols="12" sm="4">
|
|
|
+ <div class="field-label">规则 / 字段</div>
|
|
|
+ <div>{{ ruleLabel(selectedIssue.rule_code) }}</div>
|
|
|
+ <div class="text-caption">{{ selectedIssue.field_name }}</div>
|
|
|
+ </v-col>
|
|
|
+ <v-col cols="12" sm="4">
|
|
|
+ <div class="field-label">优先级 / 发生次数</div>
|
|
|
+ <div>
|
|
|
+ {{ priorityLabel(selectedIssue.priority) }} ·
|
|
|
+ {{ recurrenceLabel(selectedIssue.occurrence_number) }}
|
|
|
+ </div>
|
|
|
+ </v-col>
|
|
|
+ </v-row>
|
|
|
+ <v-alert dense outlined type="warning" class="mt-2">
|
|
|
+ {{ selectedIssue.message }}
|
|
|
+ </v-alert>
|
|
|
+ <div class="field-label">来源证据</div>
|
|
|
+ <div class="mono mb-4">
|
|
|
+ 检查 {{ selectedIssue.source_run_uid }} /
|
|
|
+ 违规 {{ selectedIssue.source_violation_uid }} /
|
|
|
+ 资产 {{ selectedIssue.asset_uid }}
|
|
|
+ </div>
|
|
|
+ <template v-if="selectedIssue.latest_remediation">
|
|
|
+ <div class="field-label">最近整改与复核</div>
|
|
|
+ <div>{{ selectedIssue.latest_remediation.summary }}</div>
|
|
|
+ <div class="text-caption mb-4">
|
|
|
+ 第 {{ selectedIssue.latest_remediation.round_number }} 轮 ·
|
|
|
+ {{ selectedIssue.latest_remediation.review_status }}
|
|
|
+ <span v-if="selectedIssue.latest_remediation.review_note">
|
|
|
+ · {{ selectedIssue.latest_remediation.review_note }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="field-label mb-2">处理时间线</div>
|
|
|
+ <v-timeline dense>
|
|
|
+ <v-timeline-item
|
|
|
+ v-for="event in issueTimeline"
|
|
|
+ :key="event.uid"
|
|
|
+ small
|
|
|
+ color="primary"
|
|
|
+ >
|
|
|
+ <strong>{{ timelineActionLabel(event.action) }}</strong>
|
|
|
+ <div class="text-caption">
|
|
|
+ {{ displayTime(event.created_at) }} · {{ event.actor_uid }}
|
|
|
+ </div>
|
|
|
+ <div v-if="event.note" class="mt-1">{{ event.note }}</div>
|
|
|
+ </v-timeline-item>
|
|
|
+ </v-timeline>
|
|
|
+ </template>
|
|
|
+ </v-card-text>
|
|
|
+ </v-card>
|
|
|
+ </v-dialog>
|
|
|
+
|
|
|
+ <v-dialog v-model="assignDialog" max-width="620">
|
|
|
+ <v-card>
|
|
|
+ <v-card-title>分派整改负责人</v-card-title>
|
|
|
+ <v-card-text>
|
|
|
+ <v-select
|
|
|
+ v-model="issueForm.assigneeUid"
|
|
|
+ :items="assignees"
|
|
|
+ item-text="display_name"
|
|
|
+ item-value="uid"
|
|
|
+ label="负责人"
|
|
|
+ outlined
|
|
|
+ dense
|
|
|
+ >
|
|
|
+ <template v-slot:item="{ item }">
|
|
|
+ <div>
|
|
|
+ {{ item.display_name }}
|
|
|
+ <span class="text-caption text--secondary">
|
|
|
+ ({{ item.username }})
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </v-select>
|
|
|
+ <v-text-field
|
|
|
+ v-model="issueForm.dueAt"
|
|
|
+ type="datetime-local"
|
|
|
+ label="整改期限(可选)"
|
|
|
+ outlined
|
|
|
+ dense
|
|
|
+ />
|
|
|
+ </v-card-text>
|
|
|
+ <v-card-actions>
|
|
|
+ <v-spacer />
|
|
|
+ <v-btn text @click="assignDialog = false">取消</v-btn>
|
|
|
+ <v-btn
|
|
|
+ color="primary"
|
|
|
+ :disabled="!issueForm.assigneeUid"
|
|
|
+ :loading="savingIssue"
|
|
|
+ @click="assignIssue"
|
|
|
+ >
|
|
|
+ 确认分派
|
|
|
+ </v-btn>
|
|
|
+ </v-card-actions>
|
|
|
+ </v-card>
|
|
|
+ </v-dialog>
|
|
|
+
|
|
|
+ <v-dialog v-model="submitDialog" max-width="620">
|
|
|
+ <v-card>
|
|
|
+ <v-card-title>提交整改复核</v-card-title>
|
|
|
+ <v-card-text>
|
|
|
+ <v-textarea
|
|
|
+ v-model.trim="issueForm.summary"
|
|
|
+ label="整改说明"
|
|
|
+ hint="说明修复了什么、如何核对;证据引用可在后续轮次持续补充"
|
|
|
+ persistent-hint
|
|
|
+ outlined
|
|
|
+ />
|
|
|
+ </v-card-text>
|
|
|
+ <v-card-actions>
|
|
|
+ <v-spacer />
|
|
|
+ <v-btn text @click="submitDialog = false">取消</v-btn>
|
|
|
+ <v-btn
|
|
|
+ color="primary"
|
|
|
+ :disabled="!issueForm.summary"
|
|
|
+ :loading="savingIssue"
|
|
|
+ @click="submitIssue"
|
|
|
+ >
|
|
|
+ 提交复核
|
|
|
+ </v-btn>
|
|
|
+ </v-card-actions>
|
|
|
+ </v-card>
|
|
|
+ </v-dialog>
|
|
|
+
|
|
|
+ <v-dialog v-model="reviewDialog" max-width="620">
|
|
|
+ <v-card>
|
|
|
+ <v-card-title>复核整改结果</v-card-title>
|
|
|
+ <v-card-text>
|
|
|
+ <v-select
|
|
|
+ v-model="issueForm.verificationResult"
|
|
|
+ :items="verificationOptions"
|
|
|
+ item-text="text"
|
|
|
+ item-value="value"
|
|
|
+ label="复验结果"
|
|
|
+ outlined
|
|
|
+ dense
|
|
|
+ />
|
|
|
+ <v-text-field
|
|
|
+ v-model.trim="issueForm.verificationRunUid"
|
|
|
+ label="复验质量检查 UID(可选)"
|
|
|
+ outlined
|
|
|
+ dense
|
|
|
+ />
|
|
|
+ <v-textarea
|
|
|
+ v-model.trim="issueForm.note"
|
|
|
+ label="复核说明"
|
|
|
+ outlined
|
|
|
+ />
|
|
|
+ <v-alert dense outlined type="info">
|
|
|
+ 通过将关闭问题,未通过将退回整改中;复核人不能是本轮整改提交人。
|
|
|
+ </v-alert>
|
|
|
+ </v-card-text>
|
|
|
+ <v-card-actions>
|
|
|
+ <v-spacer />
|
|
|
+ <v-btn text @click="reviewDialog = false">取消</v-btn>
|
|
|
+ <v-btn
|
|
|
+ color="success"
|
|
|
+ :disabled="!issueForm.verificationResult || !issueForm.note"
|
|
|
+ :loading="savingIssue"
|
|
|
+ @click="reviewIssue"
|
|
|
+ >
|
|
|
+ 提交复核
|
|
|
+ </v-btn>
|
|
|
+ </v-card-actions>
|
|
|
+ </v-card>
|
|
|
+ </v-dialog>
|
|
|
+
|
|
|
+ <v-dialog v-model="reopenDialog" max-width="620">
|
|
|
+ <v-card>
|
|
|
+ <v-card-title>重新打开质量问题</v-card-title>
|
|
|
+ <v-card-text>
|
|
|
+ <v-textarea
|
|
|
+ v-model.trim="issueForm.reason"
|
|
|
+ label="重开原因"
|
|
|
+ outlined
|
|
|
+ />
|
|
|
+ <v-text-field
|
|
|
+ v-model="issueForm.dueAt"
|
|
|
+ type="datetime-local"
|
|
|
+ label="新的整改期限(可选)"
|
|
|
+ outlined
|
|
|
+ dense
|
|
|
+ />
|
|
|
+ </v-card-text>
|
|
|
+ <v-card-actions>
|
|
|
+ <v-spacer />
|
|
|
+ <v-btn text @click="reopenDialog = false">取消</v-btn>
|
|
|
+ <v-btn
|
|
|
+ color="warning"
|
|
|
+ :disabled="!issueForm.reason"
|
|
|
+ :loading="savingIssue"
|
|
|
+ @click="reopenIssue"
|
|
|
+ >
|
|
|
+ 确认重开
|
|
|
+ </v-btn>
|
|
|
+ </v-card-actions>
|
|
|
+ </v-card>
|
|
|
+ </v-dialog>
|
|
|
+
|
|
|
<v-dialog v-model="revisionDialog" max-width="980" scrollable>
|
|
|
<v-card>
|
|
|
<v-card-title>新建质量规则版本</v-card-title>
|
|
|
@@ -372,6 +780,7 @@
|
|
|
|
|
|
<script>
|
|
|
import {
|
|
|
+ assignQualityIssue,
|
|
|
bootstrapDeviceQuality,
|
|
|
createDeviceQualityVersion,
|
|
|
getDeviceQualityAssetScores,
|
|
|
@@ -379,20 +788,37 @@ import {
|
|
|
getDeviceQualityRun,
|
|
|
getDeviceQualityRuns,
|
|
|
getDeviceQualityViolations,
|
|
|
+ getQualityIssue,
|
|
|
+ getQualityIssueAssignees,
|
|
|
+ getQualityIssues,
|
|
|
+ getQualityIssueStatistics,
|
|
|
+ getQualityIssueTimeline,
|
|
|
+ importQualityIssues,
|
|
|
publishDeviceQualityVersion,
|
|
|
+ reopenQualityIssue,
|
|
|
+ reviewQualityIssue,
|
|
|
+ startQualityIssue,
|
|
|
+ submitQualityIssue,
|
|
|
runDeviceQuality
|
|
|
} from '@/api/dataDevelopment'
|
|
|
import {
|
|
|
canBootstrapOrRevise,
|
|
|
+ canEditIssues,
|
|
|
canPublish,
|
|
|
+ canReviewIssues,
|
|
|
canRun,
|
|
|
+ dueState,
|
|
|
evidenceSummary,
|
|
|
formatPassRate,
|
|
|
formatScore,
|
|
|
+ issueStatusLabel,
|
|
|
policyStatusLabel,
|
|
|
+ priorityLabel,
|
|
|
+ recurrenceLabel,
|
|
|
resultStatusLabel,
|
|
|
ruleLabel,
|
|
|
- severityLabel
|
|
|
+ severityLabel,
|
|
|
+ timelineActionLabel
|
|
|
} from './deviceQualityModel'
|
|
|
|
|
|
export default {
|
|
|
@@ -418,6 +844,53 @@ export default {
|
|
|
detailTab: 0,
|
|
|
revisionRules: [],
|
|
|
runSourceUid: '',
|
|
|
+ loadingIssues: false,
|
|
|
+ savingIssue: false,
|
|
|
+ issueDetailLoading: false,
|
|
|
+ issues: [],
|
|
|
+ issueTotal: 0,
|
|
|
+ issuePage: 1,
|
|
|
+ issuePageSize: 20,
|
|
|
+ issueStatusFilter: null,
|
|
|
+ overdueOnly: false,
|
|
|
+ issueStatistics: {},
|
|
|
+ assignees: [],
|
|
|
+ selectedViolations: [],
|
|
|
+ selectedIssue: null,
|
|
|
+ issueTimeline: [],
|
|
|
+ issueImportDialog: false,
|
|
|
+ issueDetailDialog: false,
|
|
|
+ assignDialog: false,
|
|
|
+ submitDialog: false,
|
|
|
+ reviewDialog: false,
|
|
|
+ reopenDialog: false,
|
|
|
+ issueForm: {
|
|
|
+ priority: 'high',
|
|
|
+ dueAt: '',
|
|
|
+ assigneeUid: '',
|
|
|
+ summary: '',
|
|
|
+ verificationResult: 'passed',
|
|
|
+ verificationRunUid: '',
|
|
|
+ note: '',
|
|
|
+ reason: ''
|
|
|
+ },
|
|
|
+ issueStatusOptions: [
|
|
|
+ { text: '待分派', value: 'open' },
|
|
|
+ { text: '已分派', value: 'assigned' },
|
|
|
+ { text: '整改中', value: 'in_progress' },
|
|
|
+ { text: '待复核', value: 'pending_review' },
|
|
|
+ { text: '已关闭', value: 'closed' }
|
|
|
+ ],
|
|
|
+ priorityOptions: [
|
|
|
+ { text: '低', value: 'low' },
|
|
|
+ { text: '中', value: 'medium' },
|
|
|
+ { text: '高', value: 'high' },
|
|
|
+ { text: '紧急', value: 'critical' }
|
|
|
+ ],
|
|
|
+ verificationOptions: [
|
|
|
+ { text: '通过并关闭', value: 'passed' },
|
|
|
+ { text: '不通过并退回', value: 'failed' }
|
|
|
+ ],
|
|
|
severityOptions: [
|
|
|
{ text: '提示', value: 'info' },
|
|
|
{ text: '警告', value: 'warning' },
|
|
|
@@ -452,6 +925,16 @@ export default {
|
|
|
{ text: '适用规则', value: 'evaluated_rule_count' },
|
|
|
{ text: '违规数', value: 'violation_count' },
|
|
|
{ text: '得分', value: 'score' }
|
|
|
+ ],
|
|
|
+ issueHeaders: [
|
|
|
+ { text: '问题', value: 'issue_code', sortable: false },
|
|
|
+ { text: '状态', value: 'status', sortable: false },
|
|
|
+ { text: '优先级', value: 'priority', sortable: false },
|
|
|
+ { text: '资产 UID', value: 'asset_uid', sortable: false },
|
|
|
+ { text: '复发', value: 'recurrence', sortable: false },
|
|
|
+ { text: '负责人', value: 'assignee_uid', sortable: false },
|
|
|
+ { text: '期限', value: 'due_at', sortable: false },
|
|
|
+ { text: '操作', value: 'actions', sortable: false, width: 300 }
|
|
|
]
|
|
|
}),
|
|
|
computed: {
|
|
|
@@ -467,6 +950,12 @@ export default {
|
|
|
canExecute () {
|
|
|
return canRun(this.permissions)
|
|
|
},
|
|
|
+ canEditIssueRegister () {
|
|
|
+ return canEditIssues(this.permissions)
|
|
|
+ },
|
|
|
+ canReviewIssueRegister () {
|
|
|
+ return canReviewIssues(this.permissions)
|
|
|
+ },
|
|
|
latestVersion () {
|
|
|
return this.profile.latest_version || null
|
|
|
},
|
|
|
@@ -482,6 +971,9 @@ export default {
|
|
|
runPageCount () {
|
|
|
return Math.max(1, Math.ceil(this.runTotal / this.runPageSize))
|
|
|
},
|
|
|
+ issuePageCount () {
|
|
|
+ return Math.max(1, Math.ceil(this.issueTotal / this.issuePageSize))
|
|
|
+ },
|
|
|
enabledWeight () {
|
|
|
return Number(this.revisionRules
|
|
|
.filter(item => item.enabled)
|
|
|
@@ -496,12 +988,23 @@ export default {
|
|
|
evidenceSummary,
|
|
|
formatPassRate,
|
|
|
formatScore,
|
|
|
+ issueStatusLabel,
|
|
|
policyStatusLabel,
|
|
|
+ priorityLabel,
|
|
|
+ recurrenceLabel,
|
|
|
resultStatusLabel,
|
|
|
ruleLabel,
|
|
|
severityLabel,
|
|
|
+ timelineActionLabel,
|
|
|
+ dueState,
|
|
|
async load () {
|
|
|
- await Promise.all([this.loadProfile(), this.loadRuns()])
|
|
|
+ await Promise.all([
|
|
|
+ this.loadProfile(),
|
|
|
+ this.loadRuns(),
|
|
|
+ this.loadIssues(),
|
|
|
+ this.loadIssueStatistics(),
|
|
|
+ this.loadAssignees()
|
|
|
+ ])
|
|
|
},
|
|
|
async loadProfile () {
|
|
|
this.loadingProfile = true
|
|
|
@@ -530,6 +1033,44 @@ export default {
|
|
|
this.loadingRuns = false
|
|
|
}
|
|
|
},
|
|
|
+ async loadIssues () {
|
|
|
+ this.loadingIssues = true
|
|
|
+ try {
|
|
|
+ const response = await getQualityIssues({
|
|
|
+ status: this.issueStatusFilter || undefined,
|
|
|
+ overdue_only: this.overdueOnly,
|
|
|
+ page: this.issuePage,
|
|
|
+ page_size: this.issuePageSize
|
|
|
+ })
|
|
|
+ const data = response.data || {}
|
|
|
+ this.issues = data.records || []
|
|
|
+ this.issueTotal = data.total || 0
|
|
|
+ } catch (error) {
|
|
|
+ this.$snackbar.error(error)
|
|
|
+ } finally {
|
|
|
+ this.loadingIssues = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async loadIssueStatistics () {
|
|
|
+ try {
|
|
|
+ const response = await getQualityIssueStatistics()
|
|
|
+ this.issueStatistics = response.data || {}
|
|
|
+ } catch (error) {
|
|
|
+ this.$snackbar.error(error)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async loadAssignees () {
|
|
|
+ try {
|
|
|
+ const response = await getQualityIssueAssignees()
|
|
|
+ this.assignees = (response.data && response.data.records) || []
|
|
|
+ } catch (error) {
|
|
|
+ this.$snackbar.error(error)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async reloadIssues () {
|
|
|
+ this.issuePage = 1
|
|
|
+ await this.loadIssues()
|
|
|
+ },
|
|
|
async bootstrap () {
|
|
|
this.saving = true
|
|
|
try {
|
|
|
@@ -600,6 +1141,7 @@ export default {
|
|
|
this.detailTab = 0
|
|
|
this.selectedRun = null
|
|
|
this.violations = []
|
|
|
+ this.selectedViolations = []
|
|
|
this.assetScores = []
|
|
|
try {
|
|
|
const [detail, violations, scores] = await Promise.all([
|
|
|
@@ -625,6 +1167,189 @@ export default {
|
|
|
this.detailLoading = false
|
|
|
}
|
|
|
},
|
|
|
+ async createIssues () {
|
|
|
+ this.savingIssue = true
|
|
|
+ try {
|
|
|
+ const response = await importQualityIssues({
|
|
|
+ violation_uids: this.selectedViolations.map(item => item.uid),
|
|
|
+ priority: this.issueForm.priority,
|
|
|
+ due_at: this.toIso(this.issueForm.dueAt)
|
|
|
+ })
|
|
|
+ const data = response.data || {}
|
|
|
+ this.$snackbar.success(
|
|
|
+ `已创建 ${data.created_count || 0} 个问题,复用 ${data.existing_count || 0} 个未关闭问题`
|
|
|
+ )
|
|
|
+ this.issueImportDialog = false
|
|
|
+ this.selectedViolations = []
|
|
|
+ await Promise.all([this.loadIssues(), this.loadIssueStatistics()])
|
|
|
+ } catch (error) {
|
|
|
+ this.$snackbar.error(error)
|
|
|
+ } finally {
|
|
|
+ this.savingIssue = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async openIssue (item) {
|
|
|
+ this.issueDetailDialog = true
|
|
|
+ this.issueDetailLoading = true
|
|
|
+ this.selectedIssue = null
|
|
|
+ this.issueTimeline = []
|
|
|
+ try {
|
|
|
+ const [detail, timeline] = await Promise.all([
|
|
|
+ getQualityIssue(item.uid),
|
|
|
+ getQualityIssueTimeline(item.uid)
|
|
|
+ ])
|
|
|
+ this.selectedIssue = detail.data
|
|
|
+ this.issueTimeline = (timeline.data && timeline.data.records) || []
|
|
|
+ } catch (error) {
|
|
|
+ this.$snackbar.error(error)
|
|
|
+ this.issueDetailDialog = false
|
|
|
+ } finally {
|
|
|
+ this.issueDetailLoading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ resetIssueForm () {
|
|
|
+ this.issueForm = {
|
|
|
+ priority: 'high',
|
|
|
+ dueAt: '',
|
|
|
+ assigneeUid: '',
|
|
|
+ summary: '',
|
|
|
+ verificationResult: 'passed',
|
|
|
+ verificationRunUid: '',
|
|
|
+ note: '',
|
|
|
+ reason: ''
|
|
|
+ }
|
|
|
+ },
|
|
|
+ openAssign (item) {
|
|
|
+ this.resetIssueForm()
|
|
|
+ this.selectedIssue = item
|
|
|
+ this.issueForm.assigneeUid = item.assignee_uid || ''
|
|
|
+ this.issueForm.dueAt = this.toInputTime(item.due_at)
|
|
|
+ this.assignDialog = true
|
|
|
+ },
|
|
|
+ async assignIssue () {
|
|
|
+ this.savingIssue = true
|
|
|
+ try {
|
|
|
+ await assignQualityIssue(this.selectedIssue.uid, {
|
|
|
+ assignee_uid: this.issueForm.assigneeUid,
|
|
|
+ due_at: this.toIso(this.issueForm.dueAt),
|
|
|
+ expected_version: this.selectedIssue.current_version
|
|
|
+ })
|
|
|
+ this.$snackbar.success('整改负责人和期限已更新')
|
|
|
+ this.assignDialog = false
|
|
|
+ await this.afterIssueMutation()
|
|
|
+ } catch (error) {
|
|
|
+ this.$snackbar.error(error)
|
|
|
+ } finally {
|
|
|
+ this.savingIssue = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async startIssue (item) {
|
|
|
+ this.savingIssue = true
|
|
|
+ try {
|
|
|
+ await startQualityIssue(item.uid, item.current_version)
|
|
|
+ this.$snackbar.success('问题已进入整改中')
|
|
|
+ await this.afterIssueMutation()
|
|
|
+ } catch (error) {
|
|
|
+ this.$snackbar.error(error)
|
|
|
+ } finally {
|
|
|
+ this.savingIssue = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ openSubmit (item) {
|
|
|
+ this.resetIssueForm()
|
|
|
+ this.selectedIssue = item
|
|
|
+ this.submitDialog = true
|
|
|
+ },
|
|
|
+ async submitIssue () {
|
|
|
+ this.savingIssue = true
|
|
|
+ try {
|
|
|
+ await submitQualityIssue(this.selectedIssue.uid, {
|
|
|
+ summary: this.issueForm.summary,
|
|
|
+ evidence_refs: [],
|
|
|
+ expected_version: this.selectedIssue.current_version
|
|
|
+ })
|
|
|
+ this.$snackbar.success('整改结果已提交独立复核')
|
|
|
+ this.submitDialog = false
|
|
|
+ await this.afterIssueMutation()
|
|
|
+ } catch (error) {
|
|
|
+ this.$snackbar.error(error)
|
|
|
+ } finally {
|
|
|
+ this.savingIssue = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ openReview (item) {
|
|
|
+ this.resetIssueForm()
|
|
|
+ this.selectedIssue = item
|
|
|
+ this.reviewDialog = true
|
|
|
+ },
|
|
|
+ async reviewIssue () {
|
|
|
+ this.savingIssue = true
|
|
|
+ try {
|
|
|
+ await reviewQualityIssue(this.selectedIssue.uid, {
|
|
|
+ verification_result: this.issueForm.verificationResult,
|
|
|
+ verification_run_uid: this.issueForm.verificationRunUid || null,
|
|
|
+ note: this.issueForm.note,
|
|
|
+ expected_version: this.selectedIssue.current_version
|
|
|
+ })
|
|
|
+ this.$snackbar.success(
|
|
|
+ this.issueForm.verificationResult === 'passed'
|
|
|
+ ? '复核通过,问题已关闭'
|
|
|
+ : '复核未通过,问题已退回整改'
|
|
|
+ )
|
|
|
+ this.reviewDialog = false
|
|
|
+ await this.afterIssueMutation()
|
|
|
+ } catch (error) {
|
|
|
+ this.$snackbar.error(error)
|
|
|
+ } finally {
|
|
|
+ this.savingIssue = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ openReopen (item) {
|
|
|
+ this.resetIssueForm()
|
|
|
+ this.selectedIssue = item
|
|
|
+ this.reopenDialog = true
|
|
|
+ },
|
|
|
+ async reopenIssue () {
|
|
|
+ this.savingIssue = true
|
|
|
+ try {
|
|
|
+ await reopenQualityIssue(this.selectedIssue.uid, {
|
|
|
+ reason: this.issueForm.reason,
|
|
|
+ due_at: this.toIso(this.issueForm.dueAt),
|
|
|
+ expected_version: this.selectedIssue.current_version
|
|
|
+ })
|
|
|
+ this.$snackbar.success('质量问题已重新打开')
|
|
|
+ this.reopenDialog = false
|
|
|
+ await this.afterIssueMutation()
|
|
|
+ } catch (error) {
|
|
|
+ this.$snackbar.error(error)
|
|
|
+ } finally {
|
|
|
+ this.savingIssue = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async afterIssueMutation () {
|
|
|
+ await Promise.all([this.loadIssues(), this.loadIssueStatistics()])
|
|
|
+ },
|
|
|
+ issueStatusColor (status) {
|
|
|
+ return {
|
|
|
+ open: 'grey',
|
|
|
+ assigned: 'info',
|
|
|
+ in_progress: 'primary',
|
|
|
+ pending_review: 'warning',
|
|
|
+ closed: 'success'
|
|
|
+ }[status] || 'grey'
|
|
|
+ },
|
|
|
+ formatIssueRate (value) {
|
|
|
+ return `${(Number(value || 0) * 100).toFixed(1)}%`
|
|
|
+ },
|
|
|
+ toIso (value) {
|
|
|
+ return value ? new Date(value).toISOString() : null
|
|
|
+ },
|
|
|
+ toInputTime (value) {
|
|
|
+ if (!value) return ''
|
|
|
+ const date = new Date(value)
|
|
|
+ const local = new Date(date.getTime() - date.getTimezoneOffset() * 60000)
|
|
|
+ return local.toISOString().slice(0, 16)
|
|
|
+ },
|
|
|
displayTime (value) {
|
|
|
if (!value) return '—'
|
|
|
return new Date(value).toLocaleString('zh-CN', { hour12: false })
|