Files
X-Financial/web/src/components/shared/OperationFeedbackDialog.vue
caoxiaozhu 7989f3a159 feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL
校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计,
优化 agent 运行和编排执行链路,清理旧开发文档,前端新增
系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈
对话框和工作台日期选择器,优化报销创建和审批详情交互,
补充单元测试覆盖。
2026-05-30 15:46:51 +08:00

304 lines
6.5 KiB
Vue

<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>