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
+341
View File
@@ -0,0 +1,341 @@
<script setup lang="ts">
/**
* 工具执行轨迹卡片:让用户看清"智能体到底动了系统的哪个接口、成功没有"。
* 这是把「黑盒 AI」变成「可信执行器」的关键 —— 也让审计有据可查。
* 模块标签沿用侧栏同一套色调,保证"同一个模块在哪里都是同一个颜色"。
*/
import { ref } from "vue";
import { MODULE_LABEL, moduleMeta, moduleUrl } from "@/assets/config/modules";
import type { ToolCallTrace } from "@/types/agent";
defineProps<{ traces: ToolCallTrace[] }>();
const expanded = ref<Record<number, boolean>>({});
function toggle(i: number) {
expanded.value[i] = !expanded.value[i];
}
/** 参数展示:过滤掉内部字段,值过长时截断 */
function prettyArgs(args?: Record<string, any>): string {
if (!args || !Object.keys(args).length) return "(无参数)";
const entries = Object.entries(args).map(([k, v]) => {
let val = typeof v === "object" ? JSON.stringify(v) : String(v);
if (val.length > 80) val = val.slice(0, 80) + "…";
return `${k}: ${val}`;
});
return entries.join(",");
}
function openLanding(t: ToolCallTrace) {
const url = moduleUrl(t.module, t.landingPath);
if (url) window.open(url, "_blank");
}
/**
* 是否显示「去系统里查看」入口。
*
* - 写操作成功:去系统里确认结果(原有行为);
* - **任何失败**:服务端调不通模块用户态接口(下游 code=302,只认浏览器 CAS 会话),
* 此时由用户带自己的登录态打开模块页面,是唯一能看到真实数据的路径 —— 这是「深链兜底」。
*/
function canGo(t: ToolCallTrace): boolean {
if (!t.module) return false;
// 写操作(无论成败)→ 去系统里确认/补办;只读失败(如下游 302)→ 去系统里自己看。
// 只读成功不显示:数据已经在回复里了。
return t.write === true || !t.success;
}
/** 按钮文案:成功是"去确认",失败是"自己去看" */
function goLabel(t: ToolCallTrace): string {
return t.success ? "去系统里查看" : "去系统里自己查看";
}
</script>
<template>
<div class="trace">
<div
v-for="(t, i) in traces"
:key="i"
class="trace__item"
:class="[t.success ? 'trace__item--ok' : 'trace__item--fail']"
>
<div class="trace__head" @click="toggle(i)">
<span class="trace__badge" :class="t.write ? 'trace__badge--write' : 'trace__badge--read'">
{{ t.write ? "操作" : "查询" }}
</span>
<span class="trace__title">{{ t.title }}</span>
<span v-if="t.module" class="trace__module" :class="`tone-${moduleMeta(t.module).tone}`">
{{ MODULE_LABEL[t.module] || t.module }}
</span>
<span class="trace__spacer" />
<span class="trace__status">
<svg v-if="t.success" viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 12.5l5 5 11-11" />
</svg>
<svg v-else viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round">
<path d="M6 6l12 12M18 6L6 18" />
</svg>
<!-- 失败原因是自然语言(可能很长)→ 这里只显示一行省略号,完整内容在「详情」里看 -->
<span class="trace__status-text" :title="t.success ? '' : t.error || ''">
<template v-if="t.success">完成{{ t.costMs ? ` · ${t.costMs}ms` : "" }}</template>
<template v-else>{{ t.error || "失败" }}</template>
</span>
</span>
<span class="trace__toggle">
{{ expanded[i] ? "收起" : "详情" }}
<svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" :class="{ 'trace__chev--up': expanded[i] }">
<path d="M6 9l6 6 6-6" />
</svg>
</span>
</div>
<div v-if="expanded[i]" class="trace__body">
<div v-if="!t.success && t.error" class="trace__row">
<span class="trace__k">原因</span>
<span class="trace__v trace__v--err">{{ t.error }}</span>
</div>
<div class="trace__row">
<span class="trace__k">接口</span>
<code class="trace__code">{{ t.name }}</code>
</div>
<div class="trace__row">
<span class="trace__k">参数</span>
<span class="trace__v">{{ prettyArgs(t.arguments) }}</span>
</div>
</div>
<div v-if="canGo(t)" class="trace__actions">
<button
class="trace__go"
:class="{ 'trace__go--fallback': !t.success }"
type="button"
@click="openLanding(t)"
>
{{ goLabel(t) }}
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h13M12 6l6 6-6 6" />
</svg>
</button>
<span v-if="!t.success" class="trace__go-hint">
服务端拿不到你在业务系统的登录态,用你自己的登录态打开即可看到真实数据
</span>
</div>
</div>
</div>
</template>
<style scoped>
.trace {
display: flex;
flex-direction: column;
gap: 7px;
margin-top: 9px;
}
.trace__item {
border: 1px solid var(--agent-border);
border-left: 3px solid var(--agent-success);
border-radius: 10px;
background: var(--agent-surface);
padding: 9px 12px;
transition: box-shadow 0.16s ease;
}
.trace__item--fail {
border-left-color: var(--agent-danger);
background: #fffbfb;
}
/* ------------------------------ 头部 ------------------------------ */
.trace__head {
display: flex;
align-items: center;
gap: 9px;
cursor: pointer;
font-size: 12.5px;
min-width: 0;
}
.trace__badge {
flex: none;
padding: 2px 7px;
border-radius: 5px;
font-size: 11px;
font-weight: 500;
letter-spacing: 0.3px;
}
.trace__badge--read {
background: var(--agent-primary-soft);
color: var(--agent-primary-dark);
}
.trace__badge--write {
background: #fdf2e2;
color: var(--agent-warn);
}
.trace__title {
flex: none;
color: var(--agent-text);
font-weight: 550;
}
.trace__module {
flex: none;
padding: 2px 8px;
border-radius: 999px;
font-size: 11px;
color: var(--tone);
background: var(--tone-soft);
}
.trace__spacer {
flex: 1 1 auto;
}
.trace__status {
/* ★ 可收缩:失败原因是自然语言(可能很长),必须让它让步给「详情」按钮,
否则整张卡会被撑破、右侧按钮跑出视野(见 2026-10-10 截图) */
flex: 0 1 auto;
min-width: 0;
max-width: 46%;
display: inline-flex;
align-items: center;
gap: 4px;
color: var(--agent-success);
font-size: 12px;
}
.trace__status > svg {
flex: none;
}
.trace__status-text {
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.trace__item--fail .trace__status {
color: var(--agent-danger);
}
.trace__toggle {
flex: none;
display: inline-flex;
align-items: center;
gap: 3px;
color: var(--agent-text-mute);
font-size: 12px;
transition: color 0.16s ease;
}
.trace__head:hover .trace__toggle {
color: var(--agent-primary);
}
.trace__chev--up {
transform: rotate(180deg);
}
/* ------------------------------ 展开体 ------------------------------ */
.trace__body {
margin-top: 9px;
padding-top: 9px;
border-top: 1px dashed var(--agent-border);
font-size: 12.5px;
color: var(--agent-text-sub);
}
.trace__row {
display: flex;
gap: 9px;
margin-bottom: 5px;
word-break: break-all;
}
.trace__row:last-child {
margin-bottom: 0;
}
.trace__k {
flex: none;
width: 32px;
color: var(--agent-text-mute);
}
.trace__code {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 11.5px;
padding: 1px 6px;
border-radius: 5px;
color: var(--agent-primary-dark);
background: var(--agent-primary-soft);
}
.trace__v {
color: var(--agent-text);
}
.trace__v--err {
color: var(--agent-danger, #d4380d);
}
/* ------------------------------ 动作 ------------------------------ */
.trace__actions {
margin-top: 9px;
display: flex;
align-items: center;
gap: 9px;
flex-wrap: wrap;
}
.trace__go {
display: inline-flex;
align-items: center;
gap: 5px;
height: 28px;
padding: 0 12px;
border-radius: 8px;
border: 1px solid var(--agent-primary-line);
background: var(--agent-primary-soft);
color: var(--agent-primary-dark);
font-family: inherit;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: background 0.16s ease, border-color 0.16s ease;
}
.trace__go:hover {
background: #dfeaff;
border-color: var(--agent-primary);
}
/* 深链兜底:查询失败时用稍弱的中性色,与"成功后的去确认"区分开 */
.trace__go--fallback {
border-color: var(--agent-border);
background: #fff;
color: var(--agent-text);
}
.trace__go--fallback:hover {
background: #f6f8fb;
border-color: var(--agent-primary);
color: var(--agent-primary-dark);
}
/* 解释"为什么要去系统里看" —— 一句话说清,避免用户以为智能体坏了 */
.trace__go-hint {
flex: 1 1 auto;
min-width: 0;
font-size: 11.5px;
color: var(--agent-text-mute);
line-height: 1.4;
}
</style>