feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL 校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计, 优化 agent 运行和编排执行链路,清理旧开发文档,前端新增 系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈 对话框和工作台日期选择器,优化报销创建和审批详情交互, 补充单元测试覆盖。
This commit is contained in:
381
web/src/components/travel/RiskObservationEvidenceCard.vue
Normal file
381
web/src/components/travel/RiskObservationEvidenceCard.vue
Normal file
@@ -0,0 +1,381 @@
|
||||
<template>
|
||||
<article v-if="visible" class="detail-card panel risk-observation-evidence-card">
|
||||
<div class="detail-card-head">
|
||||
<div>
|
||||
<h3 class="detail-card-title-with-icon">
|
||||
<i class="mdi mdi-shield-search"></i>
|
||||
<span>风险证据链</span>
|
||||
</h3>
|
||||
<p>展示当前单据进入统一风险观察池后的评分、证据、图谱关系和反馈状态。</p>
|
||||
</div>
|
||||
<button
|
||||
class="risk-evidence-refresh"
|
||||
type="button"
|
||||
:disabled="loading"
|
||||
@click="loadObservations"
|
||||
>
|
||||
<i :class="loading ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-refresh'"></i>
|
||||
<span>刷新</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="risk-evidence-state">
|
||||
<i class="mdi mdi-loading mdi-spin"></i>
|
||||
<span>正在加载风险证据链</span>
|
||||
</div>
|
||||
<div v-else-if="errorMessage" class="risk-evidence-state error">
|
||||
<i class="mdi mdi-alert-circle-outline"></i>
|
||||
<span>{{ errorMessage }}</span>
|
||||
</div>
|
||||
<template v-else-if="mainObservation">
|
||||
<div class="risk-evidence-current-head">
|
||||
<div>
|
||||
<span>当前风险观察详情</span>
|
||||
<strong>{{ mainObservation.title || formatSignal(mainObservation.riskSignal) }}</strong>
|
||||
</div>
|
||||
<em>{{ activeObservationPosition }}</em>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:id="detailRegionId"
|
||||
class="risk-evidence-detail-region"
|
||||
role="region"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div class="risk-evidence-summary">
|
||||
<div class="risk-evidence-score" :class="mainObservation.riskLevel">
|
||||
<strong>{{ mainObservation.riskScore }}</strong>
|
||||
<span>{{ formatRiskLevel(mainObservation.riskLevel) }}</span>
|
||||
</div>
|
||||
<div class="risk-evidence-copy">
|
||||
<h4>{{ mainObservation.title || formatSignal(mainObservation.riskSignal) }}</h4>
|
||||
<p>{{ mainObservation.description || '暂无风险描述。' }}</p>
|
||||
<div class="risk-evidence-meta">
|
||||
<span>{{ formatSource(mainObservation.source) }}</span>
|
||||
<span>{{ mainObservation.algorithmVersion || '未记录算法版本' }}</span>
|
||||
<span>{{ formatFeedbackStatus(mainObservation.feedbackStatus) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="risk-evidence-grid">
|
||||
<section class="risk-evidence-section">
|
||||
<span class="risk-evidence-section-title">贡献分</span>
|
||||
<div class="risk-score-list">
|
||||
<div v-for="item in scoreItems" :key="item.key" class="risk-score-row">
|
||||
<span>{{ item.label }}</span>
|
||||
<i><b :style="{ width: item.width }"></b></i>
|
||||
<strong>{{ item.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="risk-evidence-section">
|
||||
<span class="risk-evidence-section-title">证据</span>
|
||||
<ul v-if="evidenceRows.length" class="risk-evidence-list">
|
||||
<li v-for="item in evidenceRows" :key="item.key">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<span>{{ item.detail }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="risk-evidence-empty">暂无证据明细。</p>
|
||||
</section>
|
||||
|
||||
<section class="risk-evidence-section">
|
||||
<span class="risk-evidence-section-title">图谱关系</span>
|
||||
<div class="risk-chip-list">
|
||||
<span v-for="item in graphItems" :key="item">{{ item }}</span>
|
||||
<em v-if="!graphItems.length">暂无图谱关系。</em>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="risk-evidence-section">
|
||||
<span class="risk-evidence-section-title">基线与建议</span>
|
||||
<div class="risk-chip-list">
|
||||
<span v-for="item in baselineAndDecisionItems" :key="item">{{ item }}</span>
|
||||
<em v-if="!baselineAndDecisionItems.length">暂无基线或建议动作。</em>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="risk-evidence-section">
|
||||
<span class="risk-evidence-section-title">制度与相似案例</span>
|
||||
<div class="risk-chip-list">
|
||||
<span v-for="item in policyAndSimilarItems" :key="item">{{ item }}</span>
|
||||
<em v-if="!policyAndSimilarItems.length">暂无制度引用或相似案例。</em>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="risk-evidence-section">
|
||||
<span class="risk-evidence-section-title">反馈历史</span>
|
||||
<ul v-if="feedbackRows.length" class="risk-evidence-list">
|
||||
<li v-for="item in feedbackRows" :key="item.key">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<span>{{ item.detail }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="risk-evidence-empty">暂无人工反馈。</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="observations.length > 1" class="risk-observation-list">
|
||||
<button
|
||||
v-for="(item, index) in observations"
|
||||
:key="observationIdentity(item, index)"
|
||||
type="button"
|
||||
class="risk-observation-row"
|
||||
:class="{ active: isActiveObservation(item, index) }"
|
||||
:aria-pressed="isActiveObservation(item, index)"
|
||||
:aria-controls="detailRegionId"
|
||||
@click="selectObservation(item, index)"
|
||||
>
|
||||
<span>{{ formatRiskLevel(item.riskLevel) }}</span>
|
||||
<strong>{{ item.title || formatSignal(item.riskSignal) }}</strong>
|
||||
<em>{{ item.riskScore }}</em>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { fetchClaimRiskObservations } from '../../services/riskObservations.js'
|
||||
|
||||
const props = defineProps({
|
||||
claimId: { type: String, default: '' }
|
||||
})
|
||||
|
||||
const observations = ref([])
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const activeObservationKey = ref('')
|
||||
const detailRegionId = 'risk-observation-active-detail'
|
||||
let loadSequence = 0
|
||||
|
||||
const visible = computed(() =>
|
||||
loading.value || Boolean(errorMessage.value) || observations.value.length > 0
|
||||
)
|
||||
const mainObservation = computed(() => {
|
||||
if (!observations.value.length) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
observations.value.find((item, index) => isActiveObservation(item, index))
|
||||
|| observations.value[0]
|
||||
)
|
||||
})
|
||||
const activeObservationPosition = computed(() => {
|
||||
if (observations.value.length <= 1) {
|
||||
return '单条观察'
|
||||
}
|
||||
const activeIndex = observations.value.findIndex((item, index) =>
|
||||
isActiveObservation(item, index)
|
||||
)
|
||||
return activeIndex >= 0 ? `${activeIndex + 1} / ${observations.value.length}` : '未选择'
|
||||
})
|
||||
const scoreItems = computed(() => {
|
||||
const scores = mainObservation.value?.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}%`
|
||||
}
|
||||
})
|
||||
})
|
||||
const evidenceRows = computed(() =>
|
||||
(mainObservation.value?.evidence || []).slice(0, 6).map((item, index) => ({
|
||||
key: `${item.code || item.title || index}`,
|
||||
title: String(item.title || item.code || item.source || `证据 ${index + 1}`).trim(),
|
||||
detail: String(item.detail || item.message || item.summary || '').trim() || '已记录证据。'
|
||||
}))
|
||||
)
|
||||
const graphItems = computed(() => [
|
||||
...(mainObservation.value?.graphNodeKeys || []).slice(0, 4),
|
||||
...(mainObservation.value?.graphEdgeKeys || []).slice(0, 4)
|
||||
].map(formatChipValue).filter(Boolean))
|
||||
const baselineAndDecisionItems = computed(() => [
|
||||
...formatRecordItems(mainObservation.value?.baseline, '基线'),
|
||||
...formatRecordItems(mainObservation.value?.decisionTrace, '建议')
|
||||
])
|
||||
const policyAndSimilarItems = computed(() => [
|
||||
...(mainObservation.value?.policyRefs || [])
|
||||
.map(formatChipValue)
|
||||
.filter(Boolean)
|
||||
.map((item) => `制度:${item}`),
|
||||
...(mainObservation.value?.similarCaseClaimIds || [])
|
||||
.map(formatChipValue)
|
||||
.filter(Boolean)
|
||||
.map((item) => `相似案例:${item}`)
|
||||
])
|
||||
const feedbackRows = computed(() =>
|
||||
(mainObservation.value?.feedbackItems || []).slice(0, 5).map((item, index) => {
|
||||
const feedbackType = item.feedbackType || item.feedback_type || item.type
|
||||
const actor = String(item.actor || item.createdBy || item.created_by || '未记录人员').trim()
|
||||
const createdAt = String(item.createdAt || item.created_at || '').trim()
|
||||
const comment = String(item.comment || item.remark || item.message || '').trim()
|
||||
return {
|
||||
key: `${item.id || createdAt || index}`,
|
||||
title: `${formatFeedbackType(feedbackType)} · ${actor}`,
|
||||
detail: [comment || '已记录反馈。', createdAt].filter(Boolean).join(' · ')
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.claimId,
|
||||
() => {
|
||||
void loadObservations()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
async function loadObservations() {
|
||||
const claimId = String(props.claimId || '').trim()
|
||||
const sequence = ++loadSequence
|
||||
errorMessage.value = ''
|
||||
if (!claimId) {
|
||||
observations.value = []
|
||||
activeObservationKey.value = ''
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const payload = await fetchClaimRiskObservations(claimId)
|
||||
if (sequence !== loadSequence) {
|
||||
return
|
||||
}
|
||||
observations.value = payload
|
||||
activeObservationKey.value = observationIdentity(payload[0], 0)
|
||||
} catch (error) {
|
||||
if (sequence === loadSequence) {
|
||||
observations.value = []
|
||||
activeObservationKey.value = ''
|
||||
errorMessage.value = error?.message || '风险证据链加载失败。'
|
||||
}
|
||||
} finally {
|
||||
if (sequence === loadSequence) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function observationIdentity(item, index = -1) {
|
||||
const explicitKey = String(item?.observationKey || item?.id || '').trim()
|
||||
if (explicitKey) {
|
||||
return explicitKey
|
||||
}
|
||||
return [
|
||||
item?.claimId,
|
||||
item?.riskSignal,
|
||||
item?.createdAt,
|
||||
index >= 0 ? index : ''
|
||||
].map((value) => String(value || '').trim()).filter(Boolean).join(':')
|
||||
}
|
||||
|
||||
function isActiveObservation(item, index = -1) {
|
||||
return observationIdentity(item, index) === activeObservationKey.value
|
||||
}
|
||||
|
||||
function selectObservation(item, index = -1) {
|
||||
const key = observationIdentity(item, index)
|
||||
if (key) {
|
||||
activeObservationKey.value = key
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
.slice(0, 6)
|
||||
.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 text = String(value || '').trim()
|
||||
const labels = {
|
||||
duplicate_invoice: '重复发票',
|
||||
split_billing: '拆分报销',
|
||||
frequent_small_claims: '高频小额',
|
||||
location_mismatch: '地点不一致',
|
||||
amount_outlier: '金额异常',
|
||||
preapproval_absent: '缺少事前申请'
|
||||
}
|
||||
return labels[text] || text.replace(/_/g, ' ') || '未知风险'
|
||||
}
|
||||
|
||||
function formatSource(value) {
|
||||
const labels = {
|
||||
financial_risk_graph: '风险图谱',
|
||||
rule_center: '规则中心'
|
||||
}
|
||||
return labels[String(value || '').trim()] || '风险观察池'
|
||||
}
|
||||
|
||||
function formatFeedbackType(value) {
|
||||
const labels = {
|
||||
confirm: '确认风险',
|
||||
confirmed: '确认风险',
|
||||
false_positive: '标记误报',
|
||||
ignore: '忽略',
|
||||
ignored: '忽略',
|
||||
resolve: '已处理',
|
||||
resolved: '已处理',
|
||||
comment: '备注'
|
||||
}
|
||||
return labels[String(value || '').trim()] || '人工反馈'
|
||||
}
|
||||
|
||||
function formatFeedbackStatus(value) {
|
||||
const labels = {
|
||||
confirmed: '已确认',
|
||||
false_positive: '已标记误报',
|
||||
ignored: '已忽略',
|
||||
resolved: '已处理',
|
||||
unreviewed: '未复核'
|
||||
}
|
||||
return labels[String(value || '').trim()] || '未复核'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped src="../../assets/styles/components/risk-observation-evidence-card.css"></style>
|
||||
@@ -170,7 +170,6 @@
|
||||
<time>{{ ui.formatFlowStepDuration(step) }}</time>
|
||||
</div>
|
||||
</header>
|
||||
<p class="flow-step-tool">工具:{{ step.tool }}</p>
|
||||
<p class="flow-step-detail">{{ ui.resolveFlowStepDetail(step) }}</p>
|
||||
<p v-if="step.error" class="flow-step-error">{{ step.error }}</p>
|
||||
</div>
|
||||
@@ -379,7 +378,6 @@
|
||||
<time>{{ ui.formatFlowStepDuration(step) }}</time>
|
||||
</div>
|
||||
</header>
|
||||
<p class="flow-step-tool">工具:{{ step.tool }}</p>
|
||||
<p class="flow-step-detail">{{ ui.resolveFlowStepDetail(step) }}</p>
|
||||
<p v-if="step.error" class="flow-step-error">{{ step.error }}</p>
|
||||
</div>
|
||||
|
||||
@@ -62,14 +62,14 @@
|
||||
role="row"
|
||||
:tabindex="row.editable && !ui.submitting && !ui.reviewActionBusy && !ui.sessionSwitchBusy ? 0 : -1"
|
||||
:aria-label="row.editable ? `编辑${row.label}` : row.label"
|
||||
@click="row.editable && !ui.isApplicationPreviewEditing(message, row.key) && !ui.submitting && !ui.reviewActionBusy && !ui.sessionSwitchBusy && ui.openApplicationPreviewEditor(message, row.key, row.value)"
|
||||
@click.stop="row.editable && !ui.isApplicationPreviewEditing(message, row.key) && !ui.submitting && !ui.reviewActionBusy && !ui.sessionSwitchBusy && ui.openApplicationPreviewEditor(message, row.key, row.value)"
|
||||
@keydown.enter.prevent="row.editable && !ui.isApplicationPreviewEditing(message, row.key) && !ui.submitting && !ui.reviewActionBusy && !ui.sessionSwitchBusy && ui.openApplicationPreviewEditor(message, row.key, row.value)"
|
||||
@keydown.space.prevent="row.editable && !ui.isApplicationPreviewEditing(message, row.key) && !ui.submitting && !ui.reviewActionBusy && !ui.sessionSwitchBusy && ui.openApplicationPreviewEditor(message, row.key, row.value)"
|
||||
>
|
||||
<span class="application-preview-label" role="cell">{{ row.label }}</span>
|
||||
<span class="application-preview-value" role="cell">
|
||||
<input
|
||||
v-if="ui.isApplicationPreviewEditing(message, row.key) && ui.resolveApplicationPreviewEditorControl(row.key) !== 'select'"
|
||||
v-if="ui.isApplicationPreviewEditing(message, row.key) && ui.resolveApplicationPreviewEditorControl(row.key) === 'text'"
|
||||
v-model="ui.applicationPreviewEditor.draftValue"
|
||||
class="application-preview-input"
|
||||
type="text"
|
||||
@@ -79,7 +79,7 @@
|
||||
@blur="ui.commitApplicationPreviewEditor(message)"
|
||||
/>
|
||||
<EnterpriseSelect
|
||||
v-else-if="ui.isApplicationPreviewEditing(message, row.key)"
|
||||
v-else-if="ui.isApplicationPreviewEditing(message, row.key) && ui.resolveApplicationPreviewEditorControl(row.key) === 'select'"
|
||||
v-model="ui.applicationPreviewEditor.draftValue"
|
||||
class="application-preview-select"
|
||||
:options="ui.resolveApplicationPreviewEditorOptions(row.key)"
|
||||
@@ -92,7 +92,10 @@
|
||||
@blur="ui.commitApplicationPreviewEditor(message)"
|
||||
/>
|
||||
<template v-else>
|
||||
<span class="application-preview-text">{{ row.value }}</span>
|
||||
<span
|
||||
class="application-preview-text"
|
||||
:class="{ 'application-preview-date-chip': row.key === 'time' && !row.missing }"
|
||||
>{{ row.value }}</span>
|
||||
<button
|
||||
v-if="row.editable"
|
||||
type="button"
|
||||
@@ -156,17 +159,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="message.role === 'assistant' && !message.reviewPayload && !message.queryPayload && message.meta?.length" class="message-meta-row">
|
||||
<span
|
||||
v-for="item in message.meta"
|
||||
:key="item"
|
||||
class="message-meta-chip"
|
||||
:class="message.metaTone"
|
||||
>
|
||||
{{ item }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="message.role === 'assistant' && !message.reviewPayload && !message.queryPayload && message.suggestedActions?.length"
|
||||
class="message-suggested-actions"
|
||||
@@ -413,12 +405,52 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="message.role === 'assistant' && !message.reviewPayload && message.draftPayload" class="draft-preview">
|
||||
<header>
|
||||
<strong>{{ message.draftPayload.title }}</strong>
|
||||
<span>待人工确认</span>
|
||||
</header>
|
||||
<pre>{{ message.draftPayload.body }}</pre>
|
||||
<div
|
||||
v-if="message.role === 'assistant' && !message.reviewPayload && message.draftPayload"
|
||||
class="draft-preview"
|
||||
:class="{ 'application-draft-preview': ui.isApplicationDraftPayload(message.draftPayload) }"
|
||||
>
|
||||
<template v-if="ui.isApplicationDraftPayload(message.draftPayload)">
|
||||
<header class="application-draft-head">
|
||||
<span class="application-draft-icon" aria-hidden="true">
|
||||
<i class="mdi mdi-file-document-check-outline"></i>
|
||||
</span>
|
||||
<span class="application-draft-title">
|
||||
<strong>申请单据已生成</strong>
|
||||
<small>已为本次业务生成申请单,请按需查看完整详情。</small>
|
||||
</span>
|
||||
<span class="application-draft-status">{{ ui.resolveApplicationDraftStatusLabel(message.draftPayload) }}</span>
|
||||
</header>
|
||||
<div class="application-draft-brief" role="group" aria-label="申请单据简要信息">
|
||||
<div
|
||||
v-for="item in ui.buildApplicationDraftSummaryItems(message.draftPayload)"
|
||||
:key="`${message.id}-application-draft-${item.label}`"
|
||||
class="application-draft-brief-item"
|
||||
:class="{ 'is-primary': item.label === '单号' }"
|
||||
>
|
||||
<span>{{ item.label }}</span>
|
||||
<strong>{{ item.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="application-draft-footer">
|
||||
<p>
|
||||
完整审批链、附件和明细可在单据详情中
|
||||
<button
|
||||
type="button"
|
||||
class="application-draft-detail-link"
|
||||
:disabled="ui.submitting || ui.reviewActionBusy || ui.sessionSwitchBusy"
|
||||
@click="ui.openApplicationDraftDetail(message)"
|
||||
>查看</button>。
|
||||
</p>
|
||||
</footer>
|
||||
</template>
|
||||
<template v-else>
|
||||
<header>
|
||||
<strong>{{ message.draftPayload.title }}</strong>
|
||||
<span>待人工确认</span>
|
||||
</header>
|
||||
<pre>{{ message.draftPayload.body }}</pre>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="message.attachments?.length" class="message-files">
|
||||
@@ -428,18 +460,35 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="ui.isOperationFeedbackVisible(message)"
|
||||
class="message-feedback-bubble"
|
||||
>
|
||||
<OperationFeedbackInlineCard
|
||||
:busy="Boolean(message.operationFeedback?.submitting)"
|
||||
:error-message="message.operationFeedback?.error || ''"
|
||||
:submitted="Boolean(message.operationFeedback?.submitted)"
|
||||
:submitted-rating="Number(message.operationFeedback?.rating || 0)"
|
||||
:reset-key="`${message.id}-${message.operationFeedback?.context?.runId || message.operationFeedback?.context?.run_id || ''}`"
|
||||
@dismiss="ui.dismissOperationFeedbackForMessage(message)"
|
||||
@submit="ui.submitOperationFeedbackForMessage(message, $event)"
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BudgetAssistantReport from './BudgetAssistantReport.vue'
|
||||
import EnterpriseSelect from '../shared/EnterpriseSelect.vue'
|
||||
import OperationFeedbackInlineCard from '../shared/OperationFeedbackInlineCard.vue'
|
||||
|
||||
export default {
|
||||
name: 'TravelReimbursementMessageItem',
|
||||
components: {
|
||||
BudgetAssistantReport,
|
||||
EnterpriseSelect
|
||||
EnterpriseSelect,
|
||||
OperationFeedbackInlineCard
|
||||
},
|
||||
props: {
|
||||
message: {
|
||||
|
||||
Reference in New Issue
Block a user