feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL 校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计, 优化 agent 运行和编排执行链路,清理旧开发文档,前端新增 系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈 对话框和工作台日期选择器,优化报销创建和审批详情交互, 补充单元测试覆盖。
This commit is contained in:
303
web/src/components/shared/OperationFeedbackDialog.vue
Normal file
303
web/src/components/shared/OperationFeedbackDialog.vue
Normal file
@@ -0,0 +1,303 @@
|
||||
<template>
|
||||
<ElDialog
|
||||
:model-value="open"
|
||||
append-to-body
|
||||
width="420px"
|
||||
:show-close="false"
|
||||
:close-on-click-modal="!busy"
|
||||
:close-on-press-escape="!busy"
|
||||
class="operation-feedback-dialog"
|
||||
modal-class="operation-feedback-overlay"
|
||||
@update:model-value="handleModelUpdate"
|
||||
@closed="resetForm"
|
||||
>
|
||||
<section class="operation-feedback">
|
||||
<header class="operation-feedback-head">
|
||||
<span class="operation-feedback-icon">
|
||||
<i class="mdi mdi-message-star-outline"></i>
|
||||
</span>
|
||||
<div>
|
||||
<h3>评价本轮处理</h3>
|
||||
<p>请给本轮智能体处理结果打分。</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="operation-feedback-stars" role="radiogroup" aria-label="本轮处理评分">
|
||||
<button
|
||||
v-for="score in scores"
|
||||
:key="score"
|
||||
type="button"
|
||||
class="operation-feedback-star"
|
||||
:class="{ active: score <= rating }"
|
||||
:disabled="busy"
|
||||
:aria-checked="rating === score ? 'true' : 'false'"
|
||||
role="radio"
|
||||
@click="rating = score"
|
||||
@mouseenter="hoverRating = score"
|
||||
@mouseleave="hoverRating = 0"
|
||||
>
|
||||
<i :class="score <= displayRating ? 'mdi mdi-star' : 'mdi mdi-star-outline'"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Transition name="feedback-reason-slide">
|
||||
<label v-if="showReasonInput" class="operation-feedback-reason">
|
||||
<span>不满意的原因</span>
|
||||
<textarea
|
||||
v-model="reason"
|
||||
maxlength="500"
|
||||
rows="3"
|
||||
:disabled="busy"
|
||||
placeholder="例如:意图识别不准、信息提取遗漏、流程引导不清晰..."
|
||||
></textarea>
|
||||
<small>{{ reason.length }}/500</small>
|
||||
</label>
|
||||
</Transition>
|
||||
|
||||
<p v-if="errorMessage" class="operation-feedback-error">{{ errorMessage }}</p>
|
||||
|
||||
<footer class="operation-feedback-actions">
|
||||
<button type="button" class="operation-feedback-secondary" :disabled="busy" @click="emit('close')">
|
||||
稍后评价
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="operation-feedback-primary"
|
||||
:disabled="busy || !rating"
|
||||
@click="submit"
|
||||
>
|
||||
<i v-if="busy" class="mdi mdi-loading mdi-spin"></i>
|
||||
<span>{{ busy ? '提交中...' : '提交评价' }}</span>
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ElDialog } from 'element-plus/es/components/dialog/index.mjs'
|
||||
|
||||
const props = defineProps({
|
||||
open: { type: Boolean, default: false },
|
||||
busy: { type: Boolean, default: false },
|
||||
errorMessage: { type: String, default: '' }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'submit'])
|
||||
const scores = [1, 2, 3, 4, 5]
|
||||
const rating = ref(0)
|
||||
const hoverRating = ref(0)
|
||||
const reason = ref('')
|
||||
const displayRating = computed(() => hoverRating.value || rating.value)
|
||||
const showReasonInput = computed(() => rating.value > 0 && rating.value <= 3)
|
||||
|
||||
function resetForm() {
|
||||
rating.value = 0
|
||||
hoverRating.value = 0
|
||||
reason.value = ''
|
||||
}
|
||||
|
||||
function handleModelUpdate(value) {
|
||||
if (!value) {
|
||||
emit('close')
|
||||
}
|
||||
}
|
||||
|
||||
function submit() {
|
||||
emit('submit', {
|
||||
rating: rating.value,
|
||||
reason: reason.value
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
(open) => {
|
||||
if (open) {
|
||||
resetForm()
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.operation-feedback-dialog) {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
:deep(.operation-feedback-dialog .el-dialog__header) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.operation-feedback-dialog .el-dialog__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.operation-feedback {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
padding: 2px 2px 0;
|
||||
}
|
||||
|
||||
.operation-feedback-head {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.operation-feedback-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 6px;
|
||||
background: #eef4ff;
|
||||
color: #1d4ed8;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.operation-feedback h3 {
|
||||
margin: 0;
|
||||
color: #172033;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.operation-feedback p {
|
||||
margin: 5px 0 0;
|
||||
color: #667085;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.operation-feedback-stars {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 6px 0 2px;
|
||||
}
|
||||
|
||||
.operation-feedback-star {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border: 1px solid #d6deea;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
color: #9aa8bb;
|
||||
cursor: pointer;
|
||||
font-size: 24px;
|
||||
transition: border-color 180ms ease, color 180ms ease, background 180ms ease;
|
||||
}
|
||||
|
||||
.operation-feedback-star:hover,
|
||||
.operation-feedback-star.active {
|
||||
border-color: #f59e0b;
|
||||
background: #fff7ed;
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.operation-feedback-star:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.operation-feedback-reason {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.operation-feedback-reason span {
|
||||
color: #344054;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.operation-feedback-reason textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
resize: vertical;
|
||||
min-height: 82px;
|
||||
border: 1px solid #d0d5dd;
|
||||
border-radius: 6px;
|
||||
padding: 10px 12px;
|
||||
color: #172033;
|
||||
font: inherit;
|
||||
line-height: 1.5;
|
||||
outline: none;
|
||||
transition: border-color 180ms ease, box-shadow 180ms ease;
|
||||
}
|
||||
|
||||
.operation-feedback-reason textarea:focus {
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 3px rgb(37 99 235 / 12%);
|
||||
}
|
||||
|
||||
.operation-feedback-reason small {
|
||||
align-self: flex-end;
|
||||
color: #98a2b3;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.operation-feedback-error {
|
||||
margin: 0;
|
||||
color: #b42318;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.operation-feedback-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.operation-feedback-secondary,
|
||||
.operation-feedback-primary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
min-width: 88px;
|
||||
height: 34px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #d0d5dd;
|
||||
padding: 0 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.operation-feedback-secondary {
|
||||
background: #fff;
|
||||
color: #344054;
|
||||
}
|
||||
|
||||
.operation-feedback-primary {
|
||||
border-color: #2563eb;
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.operation-feedback-secondary:disabled,
|
||||
.operation-feedback-primary:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.feedback-reason-slide-enter-active,
|
||||
.feedback-reason-slide-leave-active {
|
||||
transition: opacity 180ms ease, transform 180ms ease;
|
||||
}
|
||||
|
||||
.feedback-reason-slide-enter-from,
|
||||
.feedback-reason-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
</style>
|
||||
474
web/src/components/shared/OperationFeedbackInlineCard.vue
Normal file
474
web/src/components/shared/OperationFeedbackInlineCard.vue
Normal file
@@ -0,0 +1,474 @@
|
||||
<template>
|
||||
<section
|
||||
ref="feedbackRootRef"
|
||||
class="operation-feedback-inline"
|
||||
:class="{ 'is-submitted': submitted }"
|
||||
:aria-labelledby="feedbackTitleId"
|
||||
>
|
||||
<header class="operation-feedback-inline-head">
|
||||
<strong :id="feedbackTitleId">这次处理符合预期吗?</strong>
|
||||
<button
|
||||
v-if="!submitted"
|
||||
type="button"
|
||||
class="operation-feedback-link"
|
||||
:disabled="busy"
|
||||
@click="emit('dismiss')"
|
||||
>
|
||||
稍后
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div
|
||||
class="operation-feedback-stars"
|
||||
role="radiogroup"
|
||||
:aria-labelledby="feedbackTitleId"
|
||||
:aria-describedby="feedbackDescriptionId"
|
||||
@mouseleave="hoverRating = 0"
|
||||
>
|
||||
<button
|
||||
v-for="option in ratingOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
class="operation-feedback-star"
|
||||
:class="{ active: option.value === effectiveRating, preview: option.value <= displayRating }"
|
||||
:disabled="busy || submitted"
|
||||
:aria-checked="effectiveRating === option.value ? 'true' : 'false'"
|
||||
:aria-label="`${option.value}星,${option.label}`"
|
||||
:data-score="option.value"
|
||||
role="radio"
|
||||
:tabindex="resolveRatingTabIndex(option.value)"
|
||||
@click="selectRating(option.value)"
|
||||
@mouseenter="hoverRating = option.value"
|
||||
@focus="hoverRating = option.value"
|
||||
@blur="hoverRating = 0"
|
||||
@keydown="handleRatingKeydown($event, option.value)"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
:class="option.value <= displayRating ? 'mdi mdi-star' : 'mdi mdi-star-outline'"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
<p :id="feedbackDescriptionId" class="operation-feedback-status" aria-live="polite">
|
||||
{{ selectedFeedbackText }}
|
||||
</p>
|
||||
<p v-if="submitted" class="operation-feedback-thanks" aria-live="polite">
|
||||
<i class="mdi mdi-check-circle-outline" aria-hidden="true"></i>
|
||||
<span>感谢您的反馈。谢谢</span>
|
||||
</p>
|
||||
|
||||
<Transition name="feedback-reason-slide">
|
||||
<div v-if="showReasonInput" class="operation-feedback-low-rating">
|
||||
<label class="operation-feedback-reason">
|
||||
<span>哪里不符合预期?</span>
|
||||
<textarea
|
||||
v-model="reason"
|
||||
maxlength="500"
|
||||
rows="2"
|
||||
:disabled="busy"
|
||||
placeholder="例如:意图识别不准、信息提取遗漏..."
|
||||
></textarea>
|
||||
<small>{{ reason.length }}/500</small>
|
||||
</label>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="operation-feedback-primary"
|
||||
:disabled="busy || !rating"
|
||||
@click="submit"
|
||||
>
|
||||
<i v-if="busy" class="mdi mdi-loading mdi-spin"></i>
|
||||
<span>{{ busy ? '提交中...' : '提交' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<p v-if="errorMessage" class="operation-feedback-error">{{ errorMessage }}</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
busy: { type: Boolean, default: false },
|
||||
errorMessage: { type: String, default: '' },
|
||||
resetKey: { type: String, default: '' },
|
||||
submitted: { type: Boolean, default: false },
|
||||
submittedRating: { type: Number, default: 0 }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['dismiss', 'submit'])
|
||||
const feedbackIdSuffix = Math.random().toString(36).slice(2, 10)
|
||||
const feedbackTitleId = `operation-feedback-title-${feedbackIdSuffix}`
|
||||
const feedbackDescriptionId = `operation-feedback-note-${feedbackIdSuffix}`
|
||||
const ratingOptions = [
|
||||
{ value: 1, label: '很差' },
|
||||
{ value: 2, label: '不满意' },
|
||||
{ value: 3, label: '一般' },
|
||||
{ value: 4, label: '满意' },
|
||||
{ value: 5, label: '很好' }
|
||||
]
|
||||
const rating = ref(0)
|
||||
const hoverRating = ref(0)
|
||||
const reason = ref('')
|
||||
const feedbackRootRef = ref(null)
|
||||
const normalizedSubmittedRating = computed(() => {
|
||||
const score = Number(props.submittedRating || 0)
|
||||
return Number.isInteger(score) && score >= 1 && score <= 5 ? score : 0
|
||||
})
|
||||
const effectiveRating = computed(() => (props.submitted ? normalizedSubmittedRating.value || rating.value : rating.value))
|
||||
const displayRating = computed(() => (props.submitted ? effectiveRating.value : hoverRating.value || rating.value))
|
||||
const selectedOption = computed(() => ratingOptions.find((option) => option.value === effectiveRating.value) || null)
|
||||
const selectedFeedbackText = computed(() => (
|
||||
props.submitted
|
||||
? '感谢您的反馈。谢谢'
|
||||
: selectedOption.value
|
||||
? rating.value <= 3
|
||||
? `已选择 ${selectedOption.value.value} 星,可补充原因后提交。`
|
||||
: `已选择 ${selectedOption.value.value} 星,按 Enter 确认。`
|
||||
: '请选择一个评分。'
|
||||
))
|
||||
const showReasonInput = computed(() => !props.submitted && rating.value > 0 && rating.value <= 3)
|
||||
|
||||
function focusRatingButton(score) {
|
||||
nextTick(() => {
|
||||
feedbackRootRef.value
|
||||
?.querySelector(`[data-score="${score}"]`)
|
||||
?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
function selectRating(score, options = {}) {
|
||||
if (props.busy || props.submitted) {
|
||||
return
|
||||
}
|
||||
const shouldSubmitHighRating = options.submitHighRating !== false
|
||||
rating.value = score
|
||||
if (score > 3 && shouldSubmitHighRating) {
|
||||
reason.value = ''
|
||||
emit('submit', {
|
||||
rating: score,
|
||||
reason: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function resolveRatingTabIndex(score) {
|
||||
if (props.submitted) {
|
||||
return -1
|
||||
}
|
||||
return rating.value
|
||||
? rating.value === score ? 0 : -1
|
||||
: score === 1 ? 0 : -1
|
||||
}
|
||||
|
||||
function handleRatingKeydown(event, score) {
|
||||
const key = event.key
|
||||
const currentIndex = ratingOptions.findIndex((option) => option.value === score)
|
||||
if (currentIndex < 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const lastIndex = ratingOptions.length - 1
|
||||
let nextIndex = currentIndex
|
||||
if (['ArrowRight', 'ArrowDown'].includes(key)) {
|
||||
nextIndex = currentIndex === lastIndex ? 0 : currentIndex + 1
|
||||
} else if (['ArrowLeft', 'ArrowUp'].includes(key)) {
|
||||
nextIndex = currentIndex === 0 ? lastIndex : currentIndex - 1
|
||||
} else if (key === 'Home') {
|
||||
nextIndex = 0
|
||||
} else if (key === 'End') {
|
||||
nextIndex = lastIndex
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
const nextScore = ratingOptions[nextIndex].value
|
||||
selectRating(nextScore, { submitHighRating: false })
|
||||
focusRatingButton(nextScore)
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
rating.value = 0
|
||||
hoverRating.value = 0
|
||||
reason.value = ''
|
||||
}
|
||||
|
||||
function submit() {
|
||||
if (props.submitted) {
|
||||
return
|
||||
}
|
||||
emit('submit', {
|
||||
rating: rating.value,
|
||||
reason: reason.value
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.resetKey,
|
||||
() => resetForm()
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.submittedRating,
|
||||
(nextRating) => {
|
||||
const score = Number(nextRating || 0)
|
||||
if (props.submitted && Number.isInteger(score) && score >= 1 && score <= 5) {
|
||||
rating.value = score
|
||||
hoverRating.value = 0
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.operation-feedback-inline {
|
||||
width: fit-content;
|
||||
max-width: min(100%, 360px);
|
||||
display: inline-grid;
|
||||
grid-template-columns: minmax(0, auto);
|
||||
gap: 8px;
|
||||
padding: 9px 11px;
|
||||
border: 1px solid #d8e2ee;
|
||||
border-radius: 14px;
|
||||
background: #ffffff;
|
||||
color: #24324a;
|
||||
box-shadow: 0 8px 18px rgb(148 163 184 / 12%);
|
||||
}
|
||||
|
||||
.operation-feedback-inline.is-submitted {
|
||||
border-color: #d0d5dd;
|
||||
background: #f8fafc;
|
||||
color: #475467;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.operation-feedback-inline-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.operation-feedback-inline strong {
|
||||
color: #172033;
|
||||
font-size: 12px;
|
||||
font-weight: 760;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.operation-feedback-link {
|
||||
flex: 0 0 auto;
|
||||
height: 22px;
|
||||
padding: 0 6px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: #475467;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.18s ease, color 0.18s ease, background 0.18s ease;
|
||||
}
|
||||
|
||||
.operation-feedback-link:hover,
|
||||
.operation-feedback-link:focus-visible {
|
||||
border-color: #c8d5e6;
|
||||
background: #f5f8fc;
|
||||
color: #1d4ed8;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.operation-feedback-stars {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.operation-feedback-star {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: #98a2b3;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
transition: border-color 0.16s ease, color 0.16s ease, background 0.16s ease;
|
||||
}
|
||||
|
||||
.operation-feedback-star:hover,
|
||||
.operation-feedback-star:focus-visible {
|
||||
border-color: #d69a2d;
|
||||
background: #fffaf0;
|
||||
color: #b7791f;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.operation-feedback-star.preview {
|
||||
color: #b7791f;
|
||||
}
|
||||
|
||||
.operation-feedback-star.active {
|
||||
border-color: #c78315;
|
||||
background: #fff7e6;
|
||||
color: #8a4f00;
|
||||
}
|
||||
|
||||
.operation-feedback-star:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.operation-feedback-inline.is-submitted .operation-feedback-star.preview {
|
||||
color: #667085;
|
||||
}
|
||||
|
||||
.operation-feedback-inline.is-submitted .operation-feedback-star.active {
|
||||
border-color: #d0d5dd;
|
||||
background: #eef2f6;
|
||||
color: #475467;
|
||||
}
|
||||
|
||||
.operation-feedback-status {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.operation-feedback-low-rating {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
min-width: min(320px, calc(100vw - 96px));
|
||||
}
|
||||
|
||||
.operation-feedback-reason {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.operation-feedback-reason span {
|
||||
color: #344054;
|
||||
font-size: 11px;
|
||||
font-weight: 760;
|
||||
}
|
||||
|
||||
.operation-feedback-reason textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
resize: vertical;
|
||||
min-height: 54px;
|
||||
padding: 7px 9px;
|
||||
border: 1px solid #d0d5dd;
|
||||
border-radius: 4px;
|
||||
background: #ffffff;
|
||||
color: #172033;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
outline: none;
|
||||
transition: border-color 0.18s ease, box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
.operation-feedback-reason textarea:focus {
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 3px rgb(37 99 235 / 12%);
|
||||
}
|
||||
|
||||
.operation-feedback-reason small {
|
||||
justify-self: end;
|
||||
color: #98a2b3;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.operation-feedback-error {
|
||||
margin: 0;
|
||||
color: #b42318;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.operation-feedback-thanks {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
margin: 0;
|
||||
color: #475467;
|
||||
font-size: 12px;
|
||||
font-weight: 760;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.operation-feedback-thanks i {
|
||||
color: #667085;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.operation-feedback-primary {
|
||||
justify-self: end;
|
||||
min-width: 58px;
|
||||
height: 26px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #2563eb;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 760;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.18s ease, background 0.18s ease, opacity 0.18s ease;
|
||||
}
|
||||
|
||||
.operation-feedback-primary {
|
||||
background: #2563eb;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.operation-feedback-primary:hover:not(:disabled),
|
||||
.operation-feedback-primary:focus-visible {
|
||||
border-color: #1d4ed8;
|
||||
background: #1d4ed8;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.operation-feedback-link:disabled,
|
||||
.operation-feedback-primary:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.feedback-reason-slide-enter-active,
|
||||
.feedback-reason-slide-leave-active {
|
||||
transition: opacity 0.18s ease, transform 0.18s ease;
|
||||
}
|
||||
|
||||
.feedback-reason-slide-enter-from,
|
||||
.feedback-reason-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
|
||||
@media (max-width: 540px) {
|
||||
.operation-feedback-inline {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.operation-feedback-low-rating {
|
||||
min-width: min(280px, calc(100vw - 80px));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -121,6 +121,13 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="buildTraceItems(message.result).length" class="risk-sim-evidence">
|
||||
<span>执行路径</span>
|
||||
<ul>
|
||||
<li v-for="item in buildTraceItems(message.result)" :key="item">{{ item }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="message.result.missing_fields?.length" class="risk-sim-missing-fields">
|
||||
<span>待补充字段</span>
|
||||
<b v-for="field in message.result.missing_fields" :key="field.key">
|
||||
@@ -306,6 +313,7 @@ import {
|
||||
buildEvidenceItems as buildEvidenceItemsModel,
|
||||
buildRecognizedFieldRows as buildRecognizedFieldRowsModel,
|
||||
buildResultFields as buildResultFieldsModel,
|
||||
buildTraceItems as buildTraceItemsModel,
|
||||
formatDocumentMeta,
|
||||
formatFieldLabel,
|
||||
resolveFileStatusLabel,
|
||||
@@ -662,6 +670,10 @@ function buildEvidenceItems(result) {
|
||||
return buildEvidenceItemsModel(result, fields.value)
|
||||
}
|
||||
|
||||
function buildTraceItems(result) {
|
||||
return buildTraceItemsModel(result, fields.value)
|
||||
}
|
||||
|
||||
function toAttachmentPayload(file) {
|
||||
const document = file.ocrDocument || {}
|
||||
return {
|
||||
|
||||
@@ -58,6 +58,18 @@ export function buildEvidenceItems(result, fields = []) {
|
||||
return [...new Set(items)].slice(0, 5)
|
||||
}
|
||||
|
||||
export function buildTraceItems(result, fields = []) {
|
||||
const steps = Array.isArray(result?.trace?.steps) ? result.trace.steps : []
|
||||
return steps.slice(0, 6).map((step, index) => {
|
||||
const title = String(step?.title || step?.node_id || `判断 ${index + 1}`).trim()
|
||||
const status = step?.result ? '成立' : '不成立'
|
||||
const inputs = step?.inputs && typeof step.inputs === 'object'
|
||||
? Object.entries(step.inputs).slice(0, 3).map(([key, value]) => `${formatFieldName(key, fields)}=${formatDebugValue(value)}`).join(';')
|
||||
: ''
|
||||
return inputs ? `${title}:${status};${inputs}` : `${title}:${status}`
|
||||
})
|
||||
}
|
||||
|
||||
export function buildDocumentBrief(document) {
|
||||
const fields = Array.isArray(document?.document_fields) ? document.document_fields : []
|
||||
if (fields.length) {
|
||||
|
||||
Reference in New Issue
Block a user