feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL 校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计, 优化 agent 运行和编排执行链路,清理旧开发文档,前端新增 系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈 对话框和工作台日期选择器,优化报销创建和审批详情交互, 补充单元测试覆盖。
This commit is contained in:
@@ -160,7 +160,7 @@
|
||||
</template>
|
||||
|
||||
<template #table>
|
||||
<table>
|
||||
<table class="audit-asset-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ tableColumns.name }}</th>
|
||||
|
||||
@@ -67,6 +67,67 @@
|
||||
@report-saved="emit('report-saved', $event)"
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
:open="riskRuleEditOpen"
|
||||
badge="规则维护"
|
||||
badge-tone="info"
|
||||
:title="riskRuleEditMode === 'revision' ? '创建修订版本' : '编辑风险规则'"
|
||||
:description="riskRuleEditMode === 'revision' ? '已上线规则不会被直接覆盖,系统会先创建一个新的修订草稿。' : '未上线规则可以直接调整标题、费用领域、附件要求和自然语言描述。'"
|
||||
cancel-text="取消"
|
||||
:confirm-text="riskRuleEditMode === 'revision' ? '创建修订' : '保存草稿'"
|
||||
busy-text="保存中..."
|
||||
confirm-tone="primary"
|
||||
confirm-icon="mdi mdi-content-save-outline"
|
||||
:busy="riskRuleEditBusy"
|
||||
:close-on-mask="!riskRuleEditBusy"
|
||||
@close="emit('close-risk-rule-edit')"
|
||||
@confirm="emit('submit-risk-rule-edit')"
|
||||
>
|
||||
<div class="risk-rule-create-form">
|
||||
<label>
|
||||
<span>费用领域</span>
|
||||
<EnterpriseSelect
|
||||
v-model="riskRuleEditForm.expense_category"
|
||||
:options="riskRuleExpenseCategoryOptions"
|
||||
:disabled="riskRuleEditBusy"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>是否上传附件</span>
|
||||
<EnterpriseSelect
|
||||
v-model="riskRuleEditForm.requires_attachment"
|
||||
:options="riskRuleAttachmentOptions"
|
||||
:disabled="riskRuleEditBusy"
|
||||
/>
|
||||
</label>
|
||||
<label class="span-2">
|
||||
<span>规则标题</span>
|
||||
<input
|
||||
v-model="riskRuleEditForm.rule_title"
|
||||
:disabled="riskRuleEditBusy"
|
||||
maxlength="80"
|
||||
placeholder="例如:差旅目的地与票据城市一致性校验"
|
||||
/>
|
||||
</label>
|
||||
<label class="span-2">
|
||||
<span>自然语言规则</span>
|
||||
<textarea
|
||||
v-model="riskRuleEditForm.natural_language"
|
||||
:disabled="riskRuleEditBusy"
|
||||
placeholder="请用自然语言描述风险判断流程、字段、例外条件和处理动作。"
|
||||
></textarea>
|
||||
</label>
|
||||
<label v-if="riskRuleEditMode === 'revision'" class="span-2">
|
||||
<span>修订原因</span>
|
||||
<textarea
|
||||
v-model="riskRuleEditForm.change_reason"
|
||||
:disabled="riskRuleEditBusy"
|
||||
placeholder="请说明本次修订要解决的规则问题或业务变化。"
|
||||
></textarea>
|
||||
</label>
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
|
||||
<ConfirmDialog
|
||||
:open="riskRuleDeleteOpen"
|
||||
badge="删除规则"
|
||||
@@ -239,6 +300,10 @@ const props = defineProps({
|
||||
riskRuleExpenseCategoryOptions: { type: Array, default: () => [] },
|
||||
riskRuleAttachmentOptions: { type: Array, default: () => [] },
|
||||
riskRuleTestOpen: { type: Boolean, default: false },
|
||||
riskRuleEditOpen: { type: Boolean, default: false },
|
||||
riskRuleEditMode: { type: String, default: 'draft' },
|
||||
riskRuleEditForm: { type: Object, default: () => ({}) },
|
||||
riskRuleEditBusy: { type: Boolean, default: false },
|
||||
riskRuleDeleteOpen: { type: Boolean, default: false },
|
||||
riskRuleReturnOpen: { type: Boolean, default: false },
|
||||
riskRulePublishOpen: { type: Boolean, default: false },
|
||||
@@ -261,6 +326,8 @@ const emit = defineEmits([
|
||||
'submit-risk-rule-create',
|
||||
'close-risk-rule-test',
|
||||
'report-saved',
|
||||
'close-risk-rule-edit',
|
||||
'submit-risk-rule-edit',
|
||||
'close-delete-risk-rule',
|
||||
'delete-selected-risk-rule',
|
||||
'close-return-risk-rule',
|
||||
|
||||
@@ -135,7 +135,10 @@
|
||||
@click="emit('open-employee-detail', employee)"
|
||||
>
|
||||
<td>
|
||||
<strong class="doc-id">{{ employee.name }}</strong>
|
||||
<div class="digital-skill-cell">
|
||||
<span class="digital-skill-avatar" :class="employee.badgeTone">{{ employee.short }}</span>
|
||||
<strong class="doc-id">{{ employee.name }}</strong>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="doc-kind-tag application">{{ employee.skillCategory }}</span></td>
|
||||
<td>{{ employee.owner }}</td>
|
||||
|
||||
404
web/src/components/audit/DigitalEmployeeRunProducts.vue
Normal file
404
web/src/components/audit/DigitalEmployeeRunProducts.vue
Normal file
@@ -0,0 +1,404 @@
|
||||
<template>
|
||||
<article v-if="visible" class="detail-card panel run-products-card">
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<h3>本次任务产物</h3>
|
||||
<p>{{ productSubtitle }}</p>
|
||||
</div>
|
||||
<span class="edit-badge">{{ productBadge }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="run-product-state">
|
||||
<i class="mdi mdi-loading mdi-spin"></i>
|
||||
<span>正在读取本次运行产物</span>
|
||||
</div>
|
||||
<div v-else-if="errorMessage" class="run-product-state error">
|
||||
<i class="mdi mdi-alert-circle-outline"></i>
|
||||
<span>{{ errorMessage }}</span>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div v-if="metrics.length" class="json-risk-meta-grid run-product-meta-grid">
|
||||
<div
|
||||
v-for="item in metrics"
|
||||
:key="item.label"
|
||||
class="json-risk-meta-item"
|
||||
>
|
||||
<span class="json-risk-meta-label">{{ item.label }}</span>
|
||||
<span class="json-risk-meta-value">{{ item.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section v-if="productKind === 'risk_graph'" class="run-product-section">
|
||||
<div class="run-product-section-head">
|
||||
<h4>风险观察</h4>
|
||||
<span>{{ observations.length }} 条</span>
|
||||
</div>
|
||||
<div v-if="observations.length" class="run-product-observation-list">
|
||||
<article
|
||||
v-for="item in observations"
|
||||
:key="item.observationKey || item.id"
|
||||
class="run-product-observation"
|
||||
:class="{ 'is-expanded': isActiveObservation(item) }"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="toggleObservation(item)"
|
||||
@keydown.enter.prevent="toggleObservation(item)"
|
||||
@keydown.space.prevent="toggleObservation(item)"
|
||||
>
|
||||
<div class="run-product-observation-head">
|
||||
<span class="risk-level-pill" :class="item.riskLevel">
|
||||
{{ formatRiskLevel(item.riskLevel) }}
|
||||
</span>
|
||||
<strong>{{ item.title || formatSignal(item.riskSignal) }}</strong>
|
||||
<b>{{ item.riskScore }}</b>
|
||||
</div>
|
||||
<p>{{ item.description || '暂无风险描述。' }}</p>
|
||||
<div class="run-product-tags">
|
||||
<span>单据:{{ item.claimNo || item.claimId || '-' }}</span>
|
||||
<span>证据:{{ (item.evidence || []).length }}</span>
|
||||
<span>图谱关系:{{ observationGraphCount(item) }}</span>
|
||||
<span>{{ item.algorithmVersion || '未记录算法版本' }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="isActiveObservation(item)"
|
||||
class="run-product-observation-detail"
|
||||
>
|
||||
<section v-if="scoreRows(item).length">
|
||||
<span>贡献分</span>
|
||||
<div class="run-product-score-list">
|
||||
<div v-for="score in scoreRows(item)" :key="score.key">
|
||||
<em>{{ score.label }}</em>
|
||||
<i><b :style="{ width: score.width }"></b></i>
|
||||
<strong>{{ score.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section v-if="evidenceRows(item).length">
|
||||
<span>关键证据</span>
|
||||
<ul class="run-product-evidence-list">
|
||||
<li v-for="evidence in evidenceRows(item)" :key="evidence.key">
|
||||
<strong>{{ evidence.title }}</strong>
|
||||
<p>{{ evidence.detail }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section v-if="observationGraphItems(item).length">
|
||||
<span>异常关系</span>
|
||||
<div class="run-product-tags">
|
||||
<span
|
||||
v-for="graphItem in observationGraphItems(item)"
|
||||
:key="graphItem"
|
||||
>
|
||||
{{ graphItem }}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
<section v-if="observationDecisionItems(item).length">
|
||||
<span>制度与建议</span>
|
||||
<div class="run-product-tags">
|
||||
<span
|
||||
v-for="decisionItem in observationDecisionItems(item)"
|
||||
:key="decisionItem"
|
||||
>
|
||||
{{ decisionItem }}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<p v-else class="run-product-inline-empty">本次运行没有生成新的风险观察。</p>
|
||||
</section>
|
||||
|
||||
<section v-else-if="productKind === 'employee_profile'" class="run-product-section">
|
||||
<div class="run-product-section-head">
|
||||
<h4>画像快照</h4>
|
||||
<span>{{ summary.algorithm_version || '算法版本未记录' }}</span>
|
||||
</div>
|
||||
<p class="run-product-copy">
|
||||
本次产物已写入员工行为画像快照,用于后续风险图谱、审批复核和员工行为基线分析。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section v-else-if="productKind === 'knowledge'" class="run-product-section">
|
||||
<div class="run-product-section-head">
|
||||
<h4>知识制度整理</h4>
|
||||
<span>{{ summary.status || '已提交' }}</span>
|
||||
</div>
|
||||
<p class="run-product-copy">
|
||||
{{ summary.summary || '知识制度整理任务已提交,后台会继续归纳文档并刷新知识索引。' }}
|
||||
</p>
|
||||
<div class="run-product-tags">
|
||||
<span>目录:{{ summary.folder || routeJson.folder || '全部知识库' }}</span>
|
||||
<span>文档:{{ documentCount }} 份</span>
|
||||
<span v-if="summary.agent_run_id">子任务:{{ summary.agent_run_id }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { fetchRunRiskObservations } from '../../services/riskObservations.js'
|
||||
import {
|
||||
extractWorkRecordToolSummary,
|
||||
resolveWorkRecordProductKind,
|
||||
resolveWorkRecordTaskLabel
|
||||
} from '../../views/scripts/digitalEmployeeWorkRecordsModel.js'
|
||||
|
||||
const props = defineProps({
|
||||
run: { type: Object, default: null }
|
||||
})
|
||||
|
||||
const observations = ref([])
|
||||
const activeObservationKey = ref('')
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
let loadSequence = 0
|
||||
|
||||
const routeJson = computed(() =>
|
||||
props.run?.route_json && typeof props.run.route_json === 'object'
|
||||
? props.run.route_json
|
||||
: {}
|
||||
)
|
||||
const runId = computed(() => String(props.run?.run_id || '').trim())
|
||||
const productKind = computed(() => resolveWorkRecordProductKind(props.run))
|
||||
const taskLabel = computed(() => resolveWorkRecordTaskLabel(props.run) || '数字员工任务')
|
||||
const summary = computed(() => extractWorkRecordToolSummary(props.run))
|
||||
const visible = computed(() =>
|
||||
Boolean(productKind.value || loading.value || errorMessage.value)
|
||||
)
|
||||
const productSubtitle = computed(() => {
|
||||
if (productKind.value === 'risk_graph') {
|
||||
return '展示本次巡检生成的风险观察、证据数量和图谱关系计数。'
|
||||
}
|
||||
if (productKind.value === 'employee_profile') {
|
||||
return '展示本次画像巡检写入的员工画像快照摘要。'
|
||||
}
|
||||
if (productKind.value === 'knowledge') {
|
||||
return '展示本次知识制度整理任务的入队结果与处理范围。'
|
||||
}
|
||||
return '展示本次数字员工任务产生的结构化结果。'
|
||||
})
|
||||
const productBadge = computed(() => {
|
||||
if (productKind.value === 'risk_graph') {
|
||||
return '风险观察'
|
||||
}
|
||||
if (productKind.value === 'employee_profile') {
|
||||
return '画像快照'
|
||||
}
|
||||
if (productKind.value === 'knowledge') {
|
||||
return '知识整理'
|
||||
}
|
||||
return taskLabel.value
|
||||
})
|
||||
const documentCount = computed(() =>
|
||||
Array.isArray(summary.value.document_ids) ? summary.value.document_ids.length : 0
|
||||
)
|
||||
const metrics = computed(() => {
|
||||
const payload = summary.value || {}
|
||||
if (productKind.value === 'risk_graph') {
|
||||
return [
|
||||
buildMetric('扫描单据', payload.scanned_claim_count),
|
||||
buildMetric('风险观察', payload.risk_observation_count ?? observations.value.length),
|
||||
buildMetric('图谱节点', payload.graph_node_count),
|
||||
buildMetric('图谱关系', payload.graph_edge_count)
|
||||
]
|
||||
}
|
||||
if (productKind.value === 'employee_profile') {
|
||||
return [
|
||||
buildMetric('目标员工', payload.target_employee_count),
|
||||
buildMetric('画像快照', payload.snapshot_count),
|
||||
buildMetric('重点关注', payload.high_attention_employee_count),
|
||||
buildMetric('窗口期', formatWindowDays(payload.window_days))
|
||||
]
|
||||
}
|
||||
if (productKind.value === 'knowledge') {
|
||||
return [
|
||||
buildMetric('处理目录', payload.folder || routeJson.value.folder || '全部知识库'),
|
||||
buildMetric('目标文档', documentCount.value),
|
||||
buildMetric('复用任务', payload.reused ? '是' : '否'),
|
||||
buildMetric('后台 Run ID', payload.agent_run_id || '-')
|
||||
]
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [runId.value, productKind.value],
|
||||
() => {
|
||||
void loadProducts()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
async function loadProducts() {
|
||||
const currentRunId = runId.value
|
||||
const sequence = ++loadSequence
|
||||
errorMessage.value = ''
|
||||
|
||||
if (productKind.value !== 'risk_graph') {
|
||||
observations.value = []
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (!currentRunId) {
|
||||
observations.value = []
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const payload = await fetchRunRiskObservations(currentRunId)
|
||||
if (sequence !== loadSequence) {
|
||||
return
|
||||
}
|
||||
observations.value = payload
|
||||
activeObservationKey.value = payload[0]?.observationKey || payload[0]?.id || ''
|
||||
} catch (error) {
|
||||
if (sequence === loadSequence) {
|
||||
observations.value = []
|
||||
activeObservationKey.value = ''
|
||||
errorMessage.value = error?.message || '本次运行产物加载失败。'
|
||||
}
|
||||
} finally {
|
||||
if (sequence === loadSequence) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildMetric(label, value) {
|
||||
return {
|
||||
label,
|
||||
value: formatMetricValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
function formatMetricValue(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.length ? value.join(' / ') : '-'
|
||||
}
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '-'
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
function formatWindowDays(value) {
|
||||
const days = Array.isArray(value) ? value : []
|
||||
return days.length ? days.map((item) => `${item}天`).join(' / ') : '-'
|
||||
}
|
||||
|
||||
function observationGraphCount(item) {
|
||||
return (item.graphNodeKeys || []).length + (item.graphEdgeKeys || []).length
|
||||
}
|
||||
|
||||
function observationKey(item) {
|
||||
return String(item?.observationKey || item?.id || '').trim()
|
||||
}
|
||||
|
||||
function isActiveObservation(item) {
|
||||
return observationKey(item) === activeObservationKey.value
|
||||
}
|
||||
|
||||
function toggleObservation(item) {
|
||||
const key = observationKey(item)
|
||||
activeObservationKey.value = activeObservationKey.value === key ? '' : key
|
||||
}
|
||||
|
||||
function scoreRows(item) {
|
||||
const scores = item?.contributionScores || {}
|
||||
return Object.entries(scores).map(([key, value]) => {
|
||||
const numericValue = Math.max(0, Math.min(Number(value || 0), 100))
|
||||
return {
|
||||
key,
|
||||
label: formatScoreLabel(key),
|
||||
value: Math.round(numericValue),
|
||||
width: `${numericValue}%`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function evidenceRows(item) {
|
||||
return (item?.evidence || []).slice(0, 4).map((evidence, index) => ({
|
||||
key: `${evidence.code || evidence.title || index}`,
|
||||
title: String(evidence.title || evidence.code || evidence.source || `证据 ${index + 1}`).trim(),
|
||||
detail: String(evidence.detail || evidence.message || evidence.summary || '').trim() || '已记录证据。'
|
||||
}))
|
||||
}
|
||||
|
||||
function observationGraphItems(item) {
|
||||
return [
|
||||
...(item?.graphNodeKeys || []).slice(0, 5).map((value) => `节点:${formatChipValue(value)}`),
|
||||
...(item?.graphEdgeKeys || []).slice(0, 5).map((value) => `关系:${formatChipValue(value)}`)
|
||||
].filter(Boolean)
|
||||
}
|
||||
|
||||
function observationDecisionItems(item) {
|
||||
return [
|
||||
...(item?.policyRefs || []).slice(0, 4).map((value) => `制度:${formatChipValue(value)}`),
|
||||
...(item?.similarCaseClaimIds || []).slice(0, 4).map((value) => `相似案例:${formatChipValue(value)}`),
|
||||
...formatRecordItems(item?.decisionTrace, '建议').slice(0, 4)
|
||||
].filter(Boolean)
|
||||
}
|
||||
|
||||
function formatScoreLabel(value) {
|
||||
const labels = {
|
||||
S_rule: '规则命中',
|
||||
S_anomaly: '画像偏离',
|
||||
S_graph: '图谱异常',
|
||||
S_policy: '制度约束',
|
||||
S_history: '历史反馈'
|
||||
}
|
||||
return labels[value] || value
|
||||
}
|
||||
|
||||
function formatChipValue(value) {
|
||||
if (typeof value === 'string') {
|
||||
return value.trim()
|
||||
}
|
||||
if (value && typeof value === 'object') {
|
||||
return String(value.key || value.edge_key || value.node_key || JSON.stringify(value)).trim()
|
||||
}
|
||||
return String(value || '').trim()
|
||||
}
|
||||
|
||||
function formatRecordItems(value, prefix) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return []
|
||||
}
|
||||
return Object.entries(value)
|
||||
.map(([key, item]) => `${prefix}:${key}=${formatChipValue(item)}`)
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function formatRiskLevel(value) {
|
||||
const labels = {
|
||||
critical: '重大风险',
|
||||
high: '高风险',
|
||||
medium: '中风险',
|
||||
low: '低风险'
|
||||
}
|
||||
return labels[String(value || '').trim()] || '未知风险'
|
||||
}
|
||||
|
||||
function formatSignal(value) {
|
||||
const labels = {
|
||||
duplicate_invoice: '重复发票',
|
||||
split_billing: '拆分报销',
|
||||
frequent_small_claims: '高频小额',
|
||||
location_mismatch: '地点不一致',
|
||||
amount_outlier: '金额异常',
|
||||
preapproval_absent: '缺少事前申请'
|
||||
}
|
||||
const normalized = String(value || '').trim()
|
||||
return labels[normalized] || normalized.replace(/_/g, ' ') || '未知风险'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped src="../../assets/styles/components/digital-employee-run-products.css"></style>
|
||||
@@ -229,6 +229,8 @@
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<DigitalEmployeeRunProducts :run="selectedRunDetail" />
|
||||
|
||||
<!-- 卡片3:工具调用 -->
|
||||
<article class="detail-card panel">
|
||||
<div class="card-head">
|
||||
@@ -283,6 +285,7 @@
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import AuditPickerFilter from './AuditPickerFilter.vue'
|
||||
import DigitalEmployeeRunProducts from './DigitalEmployeeRunProducts.vue'
|
||||
import EnterpriseListPage from '../shared/EnterpriseListPage.vue'
|
||||
import TableLoadingState from '../shared/TableLoadingState.vue'
|
||||
import { fetchAgentRunDetail, fetchAgentRuns } from '../../services/agentAssets.js'
|
||||
@@ -308,6 +311,9 @@ defineOptions({
|
||||
name: 'DigitalEmployeeWorkRecords'
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
focusRunId: { type: String, default: '' }
|
||||
})
|
||||
const emit = defineEmits(['summary-change', 'detail-open-change', 'detail-topbar-change'])
|
||||
|
||||
const { toast } = useToast()
|
||||
@@ -559,6 +565,35 @@ function openWorkRecordDetail(run) {
|
||||
void loadWorkRecordDetail(runId)
|
||||
}
|
||||
|
||||
async function openWorkRecordById(runId) {
|
||||
const normalizedRunId = String(runId || '').trim()
|
||||
if (!normalizedRunId) {
|
||||
return
|
||||
}
|
||||
|
||||
selectedRunId.value = normalizedRunId
|
||||
selectedRunDetail.value = runs.value.find((run) => run.run_id === normalizedRunId) || {
|
||||
run_id: normalizedRunId,
|
||||
source: 'schedule',
|
||||
status: 'running',
|
||||
route_json: {},
|
||||
result_summary: '正在读取本次运行详情。'
|
||||
}
|
||||
detailOpen.value = true
|
||||
|
||||
if (!runs.value.some((run) => run.run_id === normalizedRunId)) {
|
||||
await loadWorkRecords(false)
|
||||
const refreshedRun = runs.value.find((run) => run.run_id === normalizedRunId)
|
||||
if (refreshedRun && selectedRunId.value === normalizedRunId) {
|
||||
selectedRunDetail.value = refreshedRun
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedRunId.value === normalizedRunId) {
|
||||
await loadWorkRecordDetail(normalizedRunId)
|
||||
}
|
||||
}
|
||||
|
||||
function reloadSelectedDetail() {
|
||||
if (!selectedRunId.value) {
|
||||
return
|
||||
@@ -572,6 +607,16 @@ function closeWorkRecordDetail() {
|
||||
detailError.value = ''
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.focusRunId,
|
||||
(runId) => {
|
||||
if (String(runId || '').trim()) {
|
||||
void openWorkRecordById(runId)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function startPolling() {
|
||||
stopPolling()
|
||||
pollTimer = window.setInterval(() => {
|
||||
|
||||
Reference in New Issue
Block a user