Files
yunhai-agent-front/src/components/MessageBubble.vue
T
lijing d1d76c6232
build / build (push) Successful in 2m58s
fix(form): 下拉取不到时不再退化成数字框,并让请假表单与模块页对齐
用户反馈「班级变成数字框了」的直接原因:选项加载失败(模块会话未建立时
接口回 302)后 widgetOf 把下拉降级成了数字输入框 —— 用户不可能知道
「一年级(2)班」的内部编号是 7,等于把表单搞坏。

- widgetOf 恒返回后端下发的控件形态,取不到选项时保留下拉 + 显示「重新加载」
- 请假表单对齐 HomeLeave 模块页:
  * 请假类型 = 单选按钮组 + 默认选中第一项(radio / autoPickFirst)
  * 请假时间 = 起止 datetime + 快捷时段(全天/上午/下午)+ 连续请假勾选框
    + 连续天数 + 逐天可微调时段(switch / quickPeriods / dailyExpand)
  * 事由 placeholder「请输入请假原因.....」、附件提示「可选,大小不超过 200MB」
  * 单选组 / 勾选框 / 多行文本 / 附件 / 审批流独占整行,避免挤在半列走形
- 会话预热:上限 6s→12s,且失败后总是重试两次(原「到点即弃且不重试」是
  「时不时出不来」的来源之一);callModuleApi 支持 forceWarm 供「重新加载」绕过 60s 限流
- alova 的 timeout 原先被写进 headers(fetch 不认),改为真正的客户端超时
- 处理中的转圈常驻并显示已等待秒数,不再因为气泡已有文案就静默
2026-10-13 16:15:42 +08:00

266 lines
7.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
/**
* 单条消息气泡:用户右侧、助手左侧;助手消息下挂工具执行轨迹。
* 助手侧带一个小名牌,让长对话里"谁在说话"一眼可辨。
* 文本做最小化处理:换行保留、代码块/多余 markdown 符号不渲染(保持纯文本可读)。
*/
import ToolTraceCard from "./ToolTraceCard.vue";
import FormCard from "./FormCard.vue";
import type { ChatItem } from "@/types/agent";
const props = defineProps<{ item: ChatItem; pending?: boolean; elapsed?: number }>();
const emit = defineEmits<{
(e: "form-submit", payload: { item: ChatItem; form: { tool: string; args: Record<string, any> } }): void;
}>();
/** 表单提交:冒泡给对话页统一处理(统一走同一条提交链路) */
function onFormSubmit(p: { tool: string; args: Record<string, any> }) {
emit("form-submit", { item: props.item, form: p });
}
/** 把回复拆成段落,保留换行 */
function paragraphs(text: string): string[] {
return (text || "")
.split(/\n{2,}/)
.map(s => s.trim())
.filter(Boolean);
}
</script>
<template>
<div class="msg" :class="item.role === 'user' ? 'msg--user' : 'msg--assistant'">
<div class="msg__avatar">
<svg v-if="item.role === 'assistant'" viewBox="0 0 24 24" width="17" height="17" aria-hidden="true">
<path d="M12 2.6l2.3 6.1 6.1 2.3-6.1 2.3L12 19.4l-2.3-6.1L3.6 11l6.1-2.3L12 2.6z" fill="#fff" />
</svg>
<template v-else>我</template>
</div>
<div class="msg__content">
<span v-if="item.role === 'assistant'" class="msg__name">校园智能助手</span>
<div class="msg__stack">
<!-- 正文 -->
<div v-if="item.text" class="msg__bubble">
<p v-for="(p, i) in paragraphs(item.text)" :key="i">{{ p }}</p>
</div>
<!--
处理中:**始终**显示(哪怕气泡里已经有「正在通过你的浏览器执行…」这类文案)。
历史缺陷:原来只在气泡无文案时才显示,于是服务端一下发 notice 文案,
转圈提示就消失了 —— 后续若再等十几秒,界面看着就是「卡死没下文」。
这里补上已等待秒数,让用户确知后台还在跑。
-->
<div v-if="pending" class="msg__stage" :class="{ 'msg__stage--compact': !!item.text }">
<span class="dot" />
<span class="dot" />
<span class="dot" />
<span class="msg__stage-text">{{ item.stage || "思考中…" }}</span>
<span v-if="elapsed >= 3" class="msg__stage-elapsed">已等待 {{ Math.floor(elapsed) }}s</span>
</div>
<!-- 工具轨迹 -->
<ToolTraceCard v-if="item.traces && item.traces.length" :traces="item.traces" />
<!-- 补全表单:信息不全时让用户直接填,比反复追问清晰得多 -->
<FormCard
v-if="item.form"
:form="item.form"
:submitted="item.formSubmitted"
:disabled="pending"
@submit="onFormSubmit"
/>
<!-- 错误 -->
<div v-if="item.error" class="msg__error">{{ item.error }}</div>
</div>
</div>
</div>
</template>
<style scoped>
.msg {
display: flex;
gap: 12px;
margin-bottom: 22px;
align-items: flex-start;
}
.msg--user {
flex-direction: row-reverse;
}
.msg--assistant {
animation: rise 0.28s ease both;
}
@keyframes rise {
from {
opacity: 0;
transform: translateY(6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* ------------------------------ 头像 ------------------------------ */
.msg__avatar {
flex: none;
width: 34px;
height: 34px;
border-radius: 11px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12.5px;
font-weight: 600;
color: #fff;
background: linear-gradient(140deg, #3b82f6, #7c3aed);
box-shadow: 0 8px 16px -10px rgba(59, 130, 246, 0.95);
}
.msg--user .msg__avatar {
background: linear-gradient(140deg, #64748b, #475569);
box-shadow: 0 8px 16px -10px rgba(71, 85, 105, 0.9);
}
/* ------------------------------ 内容列 ------------------------------ */
.msg__content {
max-width: 84%;
min-width: 0;
display: flex;
flex-direction: column;
}
.msg--assistant .msg__content {
align-items: flex-start;
}
.msg--user .msg__content {
align-items: flex-end;
}
.msg__name {
font-size: 11.5px;
color: var(--agent-text-mute);
margin: 0 0 6px 3px;
letter-spacing: 0.3px;
}
.msg__stack {
display: flex;
flex-direction: column;
align-items: stretch;
min-width: 0;
max-width: 100%;
}
/* ------------------------------ 气泡 ------------------------------ */
.msg__bubble {
padding: 11px 15px;
border-radius: 14px;
font-size: 14px;
line-height: 1.75;
color: var(--agent-text);
background: var(--agent-surface);
border: 1px solid var(--agent-border);
box-shadow: var(--agent-shadow-xs);
word-break: break-word;
}
.msg__bubble p {
margin: 0 0 7px;
white-space: pre-wrap;
}
.msg__bubble p:last-child {
margin-bottom: 0;
}
.msg--user .msg__bubble {
background: linear-gradient(135deg, #2563eb, #4f46e5);
border-color: transparent;
color: #fff;
box-shadow: 0 12px 24px -14px rgba(37, 99, 235, 0.85);
}
/* ------------------------------ 阶段提示 ------------------------------ */
.msg__stage {
display: inline-flex;
align-items: center;
gap: 5px;
align-self: flex-start;
padding: 10px 15px;
border-radius: 14px;
background: var(--agent-surface);
border: 1px solid var(--agent-border);
box-shadow: var(--agent-shadow-xs);
font-size: 13px;
color: var(--agent-text-sub);
}
.msg__stage-text {
margin-left: 5px;
}
/* 气泡已有文案时的紧凑版:更淡、更小,一行挂在气泡下方,只做「还在跑」的提示 */
.msg__stage--compact {
margin-top: 8px;
padding: 7px 12px;
font-size: 12.5px;
background: transparent;
border-style: dashed;
}
.msg__stage-elapsed {
margin-left: 6px;
padding-left: 8px;
border-left: 1px solid var(--agent-border-strong);
color: var(--agent-text-mute);
font-variant-numeric: tabular-nums;
}
.dot {
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--agent-primary);
animation: bounce 1.2s infinite ease-in-out;
}
.dot:nth-child(2) {
animation-delay: 0.15s;
}
.dot:nth-child(3) {
animation-delay: 0.3s;
}
@keyframes bounce {
0%,
80%,
100% {
transform: translateY(0);
opacity: 0.35;
}
40% {
transform: translateY(-4px);
opacity: 1;
}
}
/* ------------------------------ 错误 ------------------------------ */
.msg__error {
margin-top: 8px;
padding: 9px 13px;
border-radius: 10px;
background: var(--agent-danger-soft);
border: 1px solid #f8c9c5;
color: var(--agent-danger);
font-size: 13px;
}
</style>