feat: 提交校园智能助手前端工程源码(Vue3 + Vite + view-ui-plus)
build / build (push) Successful in 3m14s

This commit is contained in:
lijing
2026-10-12 14:45:00 +08:00
parent 96bb37c89f
commit 37d292c5d2
51 changed files with 13430 additions and 2 deletions
+242
View File
@@ -0,0 +1,242 @@
<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 }>();
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="pending" class="msg__stage">
<span class="dot" />
<span class="dot" />
<span class="dot" />
<span class="msg__stage-text">{{ item.stage || "思考中…" }}</span>
</div>
<!-- 正文 -->
<div v-if="item.text" class="msg__bubble">
<p v-for="(p, i) in paragraphs(item.text)" :key="i">{{ p }}</p>
</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;
}
.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>