104 lines
3.8 KiB
Vue
104 lines
3.8 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* 应用外壳:左侧导航栏 + 右侧内容区。
|
||
*
|
||
* 职责划分:
|
||
* - 这里统一拉取「当前用户」和「能力目录」(两者都是全局信息,侧栏和对话页都要用),
|
||
* 再往下传,避免各组件各拉一次。
|
||
* - 会话状态留在对话页内部,侧栏的「新建对话」通过组件 ref 调用它的方法触发。
|
||
*/
|
||
import { computed, onMounted, ref } from "vue";
|
||
import { Message } from "view-ui-plus";
|
||
import { agentApi, baseApi } from "@/api/module/agent";
|
||
import AppSidebar from "@/components/AppSidebar.vue";
|
||
import type { AgentToolCatalog, UserInfoVo } from "@/types/agent";
|
||
|
||
const user = ref<UserInfoVo | null>(null);
|
||
const catalog = ref<AgentToolCatalog | null>(null);
|
||
const loading = ref(true);
|
||
const online = ref(false);
|
||
/** 对话页组件实例(用于从侧栏触发"新建对话") */
|
||
const chatRef = ref<any>(null);
|
||
|
||
const statusText = computed(() => (loading.value ? "正在连接" : online.value ? "服务已连接" : "服务未连接"));
|
||
|
||
const toolCount = computed(() => catalog.value?.total ?? 0);
|
||
const writeCount = computed(() => {
|
||
const mods = catalog.value?.modules || {};
|
||
let n = 0;
|
||
Object.keys(mods).forEach(k => mods[k].forEach(t => t.write && n++));
|
||
return n;
|
||
});
|
||
|
||
/**
|
||
* 拉取当前登录用户(平台 CAS 会话)。
|
||
*
|
||
* 未登录时后端 CasSsoApiRedirectFilter 返回 code=302 + urlToRedirectTo,
|
||
* alova 的 errorHandle 收到 302 会直接整页跳转登录页,并返回一个**永不 resolve** 的 Promise,
|
||
* 所以跳转链路根本不会落到下面的 catch —— 这里只需处理「服务没起来 / 接口报错」。
|
||
*/
|
||
async function loadUser() {
|
||
try {
|
||
const info = await baseApi.getUserInfo().send();
|
||
user.value = (info || null) as UserInfoVo;
|
||
online.value = true;
|
||
} catch (e: any) {
|
||
console.warn("载入用户信息失败", e);
|
||
Message.error("载入用户信息失败,请检查智能体服务是否已启动");
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
/** 能力目录:拉取失败不阻塞对话(只是侧栏模块列表为空) */
|
||
async function loadCatalog() {
|
||
try {
|
||
catalog.value = (await agentApi.tools().send()) as AgentToolCatalog;
|
||
online.value = true;
|
||
} catch (e) {
|
||
console.warn("能力目录拉取失败", e);
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadUser();
|
||
loadCatalog();
|
||
});
|
||
|
||
function onNewSession() {
|
||
chatRef.value?.newSession?.();
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="agent-shell">
|
||
<AppSidebar :catalog="catalog" :user="user" @pick="t => chatRef?.pickSample?.(t)" @new-session="onNewSession" />
|
||
|
||
<section class="agent-main">
|
||
<header class="agent-topbar">
|
||
<div class="agent-topbar__titles">
|
||
<h1>智能对话</h1>
|
||
<p>说出需求,我直接调用系统接口帮你办</p>
|
||
</div>
|
||
<div class="agent-topbar__right">
|
||
<span class="agent-status" :class="{ 'agent-status--off': !loading && !online }">
|
||
<i class="agent-status__dot" />
|
||
{{ statusText }}
|
||
</span>
|
||
<span v-if="toolCount" class="agent-capsule">
|
||
可用能力 <strong class="agent-num">{{ toolCount }}</strong> 项
|
||
<i class="agent-capsule__sep" />
|
||
可直接提交 <strong class="agent-num">{{ writeCount }}</strong> 项
|
||
</span>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="agent-body">
|
||
<router-view v-slot="{ Component }">
|
||
<component :is="Component" ref="chatRef" :user="user" :catalog="catalog" />
|
||
</router-view>
|
||
</main>
|
||
</section>
|
||
</div>
|
||
</template>
|