feat: 扩展风险规则体系、审批动态路由与预算中心列表化改造
- 新增 25+ 条风险规则(预算/报销/申请/通用类),完善风险规则模拟与反馈发布机制 - 引入费用审批动态路由、平台风险分级、预审与风险阶段管理 - 预算中心列表化改造,优化票据夹仪表盘与数字员工工作看板 - 新增 Hermes 风险线索收集器、Agent 链路追踪中心 - 扩展数字员工能力库(18 个领域 Skill)与交通费用自动预估 - 完善报销申请快速预览、权限控制与前端测试覆盖
This commit is contained in:
@@ -12,7 +12,18 @@
|
||||
:loading-message="`正在加载${activeTabLabel}资产`"
|
||||
loading-icon="mdi mdi-view-list-outline"
|
||||
:hint="hintText"
|
||||
:show-pagination="!loading && !errorMessage && visibleSkills.length > 0"
|
||||
:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:page-size-options="pageSizeOptions"
|
||||
:pages="pageNumbers"
|
||||
:show-page-size="true"
|
||||
:summary="paginationSummary"
|
||||
:total="visibleSkills.length"
|
||||
:total-pages="totalPages"
|
||||
@update:active-tab="emit('update:activeType', $event)"
|
||||
@update:current-page="currentPage = $event"
|
||||
@update:page-size="changePageSize"
|
||||
@empty-action="emit('empty-action')"
|
||||
>
|
||||
<template #filters>
|
||||
@@ -178,7 +189,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="skill in visibleSkills"
|
||||
v-for="skill in pagedSkills"
|
||||
:key="skill.id"
|
||||
:class="{ 'is-disabled': skill.usesJsonRiskRule && skill.statusValue === 'generating' }"
|
||||
@click="emit('open-asset-detail', skill)"
|
||||
@@ -221,17 +232,11 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<footer v-if="!loading && !errorMessage && visibleSkills.length" class="list-foot">
|
||||
<span class="page-summary">当前展示 {{ visibleSkills.length }} 条资产</span>
|
||||
</footer>
|
||||
</template>
|
||||
</EnterpriseListPage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import AuditPickerFilter from './AuditPickerFilter.vue'
|
||||
import EnterpriseListPage from '../shared/EnterpriseListPage.vue'
|
||||
@@ -310,9 +315,66 @@ const shellTabs = computed(() =>
|
||||
}))
|
||||
)
|
||||
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const pageSizeOptions = [10, 20, 50].map((size) => ({
|
||||
label: `${size} 条/页`,
|
||||
value: size
|
||||
}))
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(props.visibleSkills.length / pageSize.value)))
|
||||
const pagedSkills = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
return props.visibleSkills.slice(start, start + pageSize.value)
|
||||
})
|
||||
const pageNumbers = computed(() => {
|
||||
const total = totalPages.value
|
||||
if (total <= 7) {
|
||||
return Array.from({ length: total }, (_, index) => index + 1)
|
||||
}
|
||||
const start = Math.max(1, Math.min(currentPage.value - 3, total - 6))
|
||||
return Array.from({ length: 7 }, (_, index) => start + index)
|
||||
})
|
||||
const paginationSummary = computed(() =>
|
||||
`共 ${props.visibleSkills.length} 条,每页 ${pageSize.value} 条,当前第 ${currentPage.value} / ${totalPages.value} 页`
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [
|
||||
props.activeType,
|
||||
props.keyword,
|
||||
props.selectedDomain,
|
||||
props.selectedOwner,
|
||||
props.selectedRiskLevel,
|
||||
props.selectedStatus,
|
||||
props.selectedRiskScenario,
|
||||
props.selectedOnlineState,
|
||||
props.selectedEnabledState
|
||||
],
|
||||
() => {
|
||||
currentPage.value = 1
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.visibleSkills.length,
|
||||
() => {
|
||||
currentPage.value = Math.min(currentPage.value, totalPages.value)
|
||||
if (currentPage.value < 1) {
|
||||
currentPage.value = 1
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function selectFilter(type, value) {
|
||||
emit('select-filter', type, value)
|
||||
}
|
||||
|
||||
function changePageSize(size) {
|
||||
const nextSize = Number(size) || 10
|
||||
pageSize.value = nextSize
|
||||
currentPage.value = 1
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped src="../../assets/styles/views/audit-view.css"></style>
|
||||
|
||||
@@ -16,6 +16,30 @@
|
||||
@confirm="emit('submit-risk-rule-create')"
|
||||
>
|
||||
<div class="risk-rule-create-form">
|
||||
<label class="span-2">
|
||||
<span>常见规则模板</span>
|
||||
<EnterpriseSelect
|
||||
v-model="selectedRiskRuleTemplateId"
|
||||
:options="riskRuleTemplateOptions"
|
||||
:disabled="riskRuleCreateBusy || riskRuleTemplatesLoading"
|
||||
:placeholder="riskRuleTemplatesLoading ? '模板加载中...' : '可选:从常见规则预填'"
|
||||
clearable
|
||||
filterable
|
||||
@change="applyRiskRuleTemplate"
|
||||
/>
|
||||
</label>
|
||||
<div v-if="selectedRiskRuleTemplate" class="risk-rule-template-preview span-2">
|
||||
<div class="risk-rule-template-preview__head">
|
||||
<strong>{{ selectedRiskRuleTemplate.title }}</strong>
|
||||
<span>{{ selectedRiskRuleTemplate.group_label }}</span>
|
||||
</div>
|
||||
<p>{{ selectedRiskRuleTemplate.description }}</p>
|
||||
<small>字段:{{ formatTemplateFields(selectedRiskRuleTemplate.fields) }}</small>
|
||||
<small>模板只预填规则文本,提交后仍走通用自然语言生成链路。</small>
|
||||
</div>
|
||||
<p v-else-if="riskRuleTemplateLoadFailed" class="risk-rule-template-note span-2">
|
||||
常见模板暂时加载失败,可以继续手动编写规则。
|
||||
</p>
|
||||
<label>
|
||||
<span>业务环节</span>
|
||||
<EnterpriseSelect
|
||||
@@ -72,9 +96,9 @@
|
||||
badge="规则维护"
|
||||
badge-tone="info"
|
||||
:title="riskRuleEditMode === 'revision' ? '创建修订版本' : '编辑风险规则'"
|
||||
:description="riskRuleEditMode === 'revision' ? '已上线规则不会被直接覆盖,系统会先创建一个新的修订草稿。' : '未上线规则可以直接调整标题、费用领域、附件要求和自然语言描述。'"
|
||||
:description="riskRuleEditMode === 'revision' ? '已上线规则不会被直接覆盖,系统会先创建一个新的修订草稿并重新生成执行模板。' : '未上线规则可以直接调整标题、费用领域、附件要求和自然语言描述,并同步重新生成执行模板。'"
|
||||
cancel-text="取消"
|
||||
:confirm-text="riskRuleEditMode === 'revision' ? '创建修订' : '保存草稿'"
|
||||
:confirm-text="riskRuleEditMode === 'revision' ? '创建并生成' : '保存并生成'"
|
||||
busy-text="保存中..."
|
||||
confirm-tone="primary"
|
||||
confirm-icon="mdi mdi-content-save-outline"
|
||||
@@ -280,10 +304,11 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import ConfirmDialog from '../shared/ConfirmDialog.vue'
|
||||
import EnterpriseSelect from '../shared/EnterpriseSelect.vue'
|
||||
import RiskRuleTestDialog from '../shared/RiskRuleTestDialog.vue'
|
||||
import { fetchRiskRuleTemplates } from '../../services/agentAssets.js'
|
||||
|
||||
defineOptions({
|
||||
name: 'AuditRuleDialogs'
|
||||
@@ -354,7 +379,123 @@ const reviewSubmitReviewerModel = computed({
|
||||
get: () => props.reviewSubmitReviewer,
|
||||
set: (value) => emit('update:reviewSubmitReviewer', value)
|
||||
})
|
||||
|
||||
const riskRuleTemplateGroups = ref([])
|
||||
const riskRuleTemplatesLoading = ref(false)
|
||||
const riskRuleTemplateLoadFailed = ref(false)
|
||||
const selectedRiskRuleTemplateId = ref('')
|
||||
|
||||
const flatRiskRuleTemplates = computed(() =>
|
||||
riskRuleTemplateGroups.value.flatMap((group) => group.templates || [])
|
||||
)
|
||||
const riskRuleTemplateOptions = computed(() =>
|
||||
flatRiskRuleTemplates.value.map((template) => ({
|
||||
value: template.template_id,
|
||||
label: `${template.group_label} / ${template.title}`
|
||||
}))
|
||||
)
|
||||
const selectedRiskRuleTemplate = computed(() =>
|
||||
flatRiskRuleTemplates.value.find((template) => template.template_id === selectedRiskRuleTemplateId.value) || null
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.riskRuleCreateOpen,
|
||||
(open) => {
|
||||
if (!open) {
|
||||
selectedRiskRuleTemplateId.value = ''
|
||||
return
|
||||
}
|
||||
loadRiskRuleTemplates()
|
||||
}
|
||||
)
|
||||
|
||||
async function loadRiskRuleTemplates() {
|
||||
if (riskRuleTemplateGroups.value.length || riskRuleTemplatesLoading.value) {
|
||||
return
|
||||
}
|
||||
riskRuleTemplatesLoading.value = true
|
||||
riskRuleTemplateLoadFailed.value = false
|
||||
try {
|
||||
riskRuleTemplateGroups.value = await fetchRiskRuleTemplates()
|
||||
} catch {
|
||||
riskRuleTemplateLoadFailed.value = true
|
||||
} finally {
|
||||
riskRuleTemplatesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function applyRiskRuleTemplate(templateId) {
|
||||
const template = flatRiskRuleTemplates.value.find((item) => item.template_id === templateId)
|
||||
if (!template || !props.riskRuleCreateForm) {
|
||||
return
|
||||
}
|
||||
|
||||
props.riskRuleCreateForm.business_domain = template.business_domain || 'expense'
|
||||
props.riskRuleCreateForm.business_stage = template.business_stage || 'reimbursement'
|
||||
props.riskRuleCreateForm.expense_category = template.expense_category || props.riskRuleCreateForm.expense_category
|
||||
props.riskRuleCreateForm.rule_title = template.title || ''
|
||||
props.riskRuleCreateForm.requires_attachment = Boolean(template.requires_attachment)
|
||||
props.riskRuleCreateForm.natural_language = template.natural_language || ''
|
||||
}
|
||||
|
||||
function formatTemplateFields(fields = []) {
|
||||
const labels = fields.map((field) => field.display || field.label || field.key).filter(Boolean)
|
||||
if (!labels.length) {
|
||||
return '未配置字段'
|
||||
}
|
||||
if (labels.length <= 4) {
|
||||
return labels.join('、')
|
||||
}
|
||||
return `${labels.slice(0, 4).join('、')} 等 ${labels.length} 项`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped src="../../assets/styles/views/audit-view.css"></style>
|
||||
<style scoped src="../../assets/styles/views/audit-view-part2.css"></style>
|
||||
<style scoped>
|
||||
.risk-rule-create-form > .span-2 {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.risk-rule-template-preview {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
padding: 12px;
|
||||
border: 1px solid #dbe5ef;
|
||||
border-radius: 6px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.risk-rule-template-preview__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.risk-rule-template-preview__head strong {
|
||||
min-width: 0;
|
||||
color: #0f172a;
|
||||
font-size: 13px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.risk-rule-template-preview__head span,
|
||||
.risk-rule-template-preview small,
|
||||
.risk-rule-template-note {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.risk-rule-template-preview p {
|
||||
margin: 0;
|
||||
color: #334155;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.risk-rule-template-note {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
:show-pagination="!loading && !errorMessage && visibleEmployees.length > 0"
|
||||
:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:page-size-options="pageSizeOptions"
|
||||
:pages="pageNumbers"
|
||||
:show-page-size="false"
|
||||
:show-page-size="true"
|
||||
:summary="paginationSummary"
|
||||
:total="visibleEmployees.length"
|
||||
:total-pages="totalPages"
|
||||
@@ -18,6 +19,8 @@
|
||||
loading-message="正在加载数字员工资产"
|
||||
loading-icon="mdi mdi-view-list-outline"
|
||||
@update:current-page="currentPage = $event"
|
||||
@update:page-size="pageSize = $event"
|
||||
@page-size-change="changePageSize"
|
||||
>
|
||||
<template #filters>
|
||||
<label class="list-search">
|
||||
@@ -43,6 +46,19 @@
|
||||
@select="selectFilter('status', $event)"
|
||||
/>
|
||||
|
||||
<AuditPickerFilter
|
||||
id="skillCategory"
|
||||
title="选择技能类型"
|
||||
close-label="关闭技能类型选择"
|
||||
:active-filter-popover="activeFilterPopover"
|
||||
:label="selectedSkillCategoryLabel"
|
||||
:options="skillCategoryOptions"
|
||||
:selected-value="selectedSkillCategory"
|
||||
@toggle="emit('toggle-filter-popover', $event)"
|
||||
@close="emit('close-filter-popover')"
|
||||
@select="selectFilter('skillCategory', $event)"
|
||||
/>
|
||||
|
||||
<AuditPickerFilter
|
||||
id="enabled"
|
||||
title="选择启动状态"
|
||||
@@ -172,6 +188,9 @@ const props = defineProps({
|
||||
selectedStatus: { type: String, default: '' },
|
||||
selectedStatusLabel: { type: String, default: '' },
|
||||
statusOptions: { type: Array, default: () => [] },
|
||||
selectedSkillCategory: { type: String, default: '' },
|
||||
selectedSkillCategoryLabel: { type: String, default: '' },
|
||||
skillCategoryOptions: { type: Array, default: () => [] },
|
||||
selectedEnabledState: { type: String, default: '' },
|
||||
selectedEnabledLabel: { type: String, default: '' },
|
||||
enabledStateOptions: { type: Array, default: () => [] },
|
||||
@@ -195,11 +214,16 @@ const emit = defineEmits([
|
||||
])
|
||||
|
||||
const currentPage = ref(1)
|
||||
const pageSize = 20
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(props.visibleEmployees.length / pageSize)))
|
||||
const pageSize = ref(10)
|
||||
const pageSizeOptions = [
|
||||
{ label: '10 条/页', value: 10 },
|
||||
{ label: '20 条/页', value: 20 },
|
||||
{ label: '50 条/页', value: 50 }
|
||||
]
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(props.visibleEmployees.length / pageSize.value)))
|
||||
const pagedEmployees = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize
|
||||
return props.visibleEmployees.slice(start, start + pageSize)
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
return props.visibleEmployees.slice(start, start + pageSize.value)
|
||||
})
|
||||
const pageNumbers = computed(() => {
|
||||
const total = totalPages.value
|
||||
@@ -210,7 +234,7 @@ const pageNumbers = computed(() => {
|
||||
return Array.from({ length: 7 }, (_, index) => start + index)
|
||||
})
|
||||
const paginationSummary = computed(() =>
|
||||
`共 ${props.visibleEmployees.length} 条,目前第 ${currentPage.value} / ${totalPages.value} 页`
|
||||
`共 ${props.visibleEmployees.length} 条,每页 ${pageSize.value} 条,目前第 ${currentPage.value} / ${totalPages.value} 页`
|
||||
)
|
||||
const emptyState = {
|
||||
eyebrow: '数字员工',
|
||||
@@ -223,7 +247,13 @@ const emptyState = {
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.keyword, props.selectedStatus, props.selectedEnabledState, props.selectedExecutionMode],
|
||||
() => [
|
||||
props.keyword,
|
||||
props.selectedStatus,
|
||||
props.selectedSkillCategory,
|
||||
props.selectedEnabledState,
|
||||
props.selectedExecutionMode
|
||||
],
|
||||
() => {
|
||||
currentPage.value = 1
|
||||
}
|
||||
@@ -243,6 +273,11 @@ watch(
|
||||
function selectFilter(type, value) {
|
||||
emit('select-filter', type, value)
|
||||
}
|
||||
|
||||
function changePageSize(size) {
|
||||
pageSize.value = Number(size) || 10
|
||||
currentPage.value = 1
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped src="../../assets/styles/views/digital-employees-view.css"></style>
|
||||
|
||||
@@ -110,6 +110,62 @@
|
||||
<p v-else class="run-product-inline-empty">本次运行没有生成新的风险观察。</p>
|
||||
</section>
|
||||
|
||||
<section v-else-if="productKind === 'risk_clue'" class="run-product-section">
|
||||
<div class="run-product-section-head">
|
||||
<h4>待复核线索</h4>
|
||||
<span>{{ riskClues.length }} 条</span>
|
||||
</div>
|
||||
<div v-if="riskClues.length" class="run-product-observation-list">
|
||||
<article
|
||||
v-for="item in riskClues"
|
||||
:key="item.clue_id || item.clueId"
|
||||
class="run-product-observation"
|
||||
>
|
||||
<div class="run-product-observation-head">
|
||||
<span class="risk-level-pill" :class="item.risk_level || item.riskLevel">
|
||||
{{ formatRiskLevel(item.risk_level || item.riskLevel) }}
|
||||
</span>
|
||||
<strong>{{ item.title || formatSignal(item.risk_signal || item.riskSignal) }}</strong>
|
||||
<b>{{ formatConfidence(item.confidence_score ?? item.confidenceScore) }}</b>
|
||||
</div>
|
||||
<p>{{ item.summary || '该线索需要人工复核事实和证据。' }}</p>
|
||||
<div class="run-product-tags">
|
||||
<span>单据:{{ item.claim_no || item.claimNo || '-' }}</span>
|
||||
<span>状态:待人工复核</span>
|
||||
<span v-if="item.observation_key || item.observationKey">
|
||||
观察:{{ item.observation_key || item.observationKey }}
|
||||
</span>
|
||||
<span v-if="item.feedback_status || item.feedbackStatus">
|
||||
反馈:{{ formatFeedbackStatus(item.feedback_status || item.feedbackStatus) }}
|
||||
</span>
|
||||
<span>规则命中:{{ (item.rule_hits || item.ruleHits || []).length }}</span>
|
||||
<span>证据:{{ (item.evidence_refs || item.evidenceRefs || []).length }}</span>
|
||||
</div>
|
||||
<p v-if="item.review_reason || item.reviewReason" class="run-product-muted-copy">
|
||||
{{ item.review_reason || item.reviewReason }}
|
||||
</p>
|
||||
<p v-if="item.next_action || item.nextAction" class="run-product-muted-copy">
|
||||
{{ item.next_action || item.nextAction }}
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
<p v-else class="run-product-inline-empty">本次运行没有整理出待复核线索。</p>
|
||||
|
||||
<div v-if="feedbackRows.length" class="run-product-feedback-panel">
|
||||
<div class="run-product-section-head compact">
|
||||
<h4>反馈样本</h4>
|
||||
<span>{{ feedbackTotal }} 条</span>
|
||||
</div>
|
||||
<ul class="run-product-feedback-list">
|
||||
<li v-for="item in feedbackRows" :key="item.feedback_id || item.feedbackId">
|
||||
<strong>{{ formatFeedbackStatus(item.feedback_type || item.feedbackType) }}</strong>
|
||||
<span>{{ item.observation_key || item.observationKey || '未关联观察' }}</span>
|
||||
<em>{{ item.actor || '系统' }}</em>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="productKind === 'employee_profile'" class="run-product-section">
|
||||
<div class="run-product-section-head">
|
||||
<h4>画像快照</h4>
|
||||
@@ -180,6 +236,9 @@ const productSubtitle = computed(() => {
|
||||
if (productKind.value === 'knowledge') {
|
||||
return '展示本次知识制度整理任务的入队结果与处理范围。'
|
||||
}
|
||||
if (productKind.value === 'risk_clue') {
|
||||
return '展示本次任务归集的事实、规则命中和待人工复核线索。'
|
||||
}
|
||||
return '展示本次数字员工任务产生的结构化结果。'
|
||||
})
|
||||
const productBadge = computed(() => {
|
||||
@@ -192,8 +251,23 @@ const productBadge = computed(() => {
|
||||
if (productKind.value === 'knowledge') {
|
||||
return '知识整理'
|
||||
}
|
||||
if (productKind.value === 'risk_clue') {
|
||||
return '线索归集'
|
||||
}
|
||||
return taskLabel.value
|
||||
})
|
||||
const riskClues = computed(() =>
|
||||
Array.isArray(summary.value.risk_clues) ? summary.value.risk_clues : []
|
||||
)
|
||||
const feedbackSummary = computed(() =>
|
||||
summary.value.feedback_summary && typeof summary.value.feedback_summary === 'object'
|
||||
? summary.value.feedback_summary
|
||||
: {}
|
||||
)
|
||||
const feedbackRows = computed(() =>
|
||||
Array.isArray(feedbackSummary.value.recent) ? feedbackSummary.value.recent.slice(0, 6) : []
|
||||
)
|
||||
const feedbackTotal = computed(() => Number(feedbackSummary.value.total || feedbackRows.value.length || 0))
|
||||
const documentCount = computed(() =>
|
||||
Array.isArray(summary.value.document_ids) ? summary.value.document_ids.length : 0
|
||||
)
|
||||
@@ -223,6 +297,14 @@ const metrics = computed(() => {
|
||||
buildMetric('后台 Run ID', payload.agent_run_id || '-')
|
||||
]
|
||||
}
|
||||
if (productKind.value === 'risk_clue') {
|
||||
return [
|
||||
buildMetric('事实', payload.fact_count ?? (payload.facts || []).length),
|
||||
buildMetric('规则命中', payload.rule_hit_count ?? (payload.rule_hits || []).length),
|
||||
buildMetric('待复核线索', payload.risk_clue_count ?? riskClues.value.length),
|
||||
buildMetric('反馈样本', feedbackTotal.value)
|
||||
]
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
@@ -387,6 +469,31 @@ function formatRiskLevel(value) {
|
||||
return labels[String(value || '').trim()] || '未知风险'
|
||||
}
|
||||
|
||||
function formatConfidence(value) {
|
||||
const numericValue = Number(value || 0)
|
||||
if (!Number.isFinite(numericValue) || numericValue <= 0) {
|
||||
return '-'
|
||||
}
|
||||
return `${Math.round(numericValue * 100)}%`
|
||||
}
|
||||
|
||||
function formatFeedbackStatus(value) {
|
||||
const labels = {
|
||||
unreviewed: '未复核',
|
||||
pending_review: '待复核',
|
||||
confirm: '已确认',
|
||||
confirmed: '已确认',
|
||||
false_positive: '误报',
|
||||
ignore: '已忽略',
|
||||
ignored: '已忽略',
|
||||
resolve: '已处理',
|
||||
resolved: '已处理',
|
||||
comment: '备注'
|
||||
}
|
||||
const normalized = String(value || '').trim()
|
||||
return labels[normalized] || normalized || '未复核'
|
||||
}
|
||||
|
||||
function formatSignal(value) {
|
||||
const labels = {
|
||||
duplicate_invoice: '重复发票',
|
||||
|
||||
@@ -270,6 +270,10 @@
|
||||
<span>返回工作记录列表</span>
|
||||
</button>
|
||||
<div class="detail-action-group">
|
||||
<button class="minor-action" type="button" :disabled="!selectedRunDetail?.run_id" @click="openTraceCenter">
|
||||
<i class="mdi mdi-timeline-text-outline"></i>
|
||||
<span>查看 Trace</span>
|
||||
</button>
|
||||
<button class="minor-action" type="button" :disabled="detailLoading" @click="reloadSelectedDetail">
|
||||
<i class="mdi mdi-refresh"></i>
|
||||
<span>{{ detailLoading ? '刷新中...' : '刷新详情' }}</span>
|
||||
@@ -283,6 +287,7 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import AuditPickerFilter from './AuditPickerFilter.vue'
|
||||
import DigitalEmployeeRunProducts from './DigitalEmployeeRunProducts.vue'
|
||||
@@ -317,6 +322,7 @@ const props = defineProps({
|
||||
const emit = defineEmits(['summary-change', 'detail-open-change', 'detail-topbar-change'])
|
||||
|
||||
const { toast } = useToast()
|
||||
const router = useRouter()
|
||||
const runs = ref([])
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
@@ -607,6 +613,14 @@ function closeWorkRecordDetail() {
|
||||
detailError.value = ''
|
||||
}
|
||||
|
||||
function openTraceCenter() {
|
||||
const runId = String(selectedRunDetail.value?.run_id || selectedRunId.value || '').trim()
|
||||
if (!runId) {
|
||||
return
|
||||
}
|
||||
router.push({ name: 'app-settings', query: { section: 'agentTraces', run_id: runId } })
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.focusRunId,
|
||||
(runId) => {
|
||||
|
||||
Reference in New Issue
Block a user