feat(ui): 实现普通用户和管理员差异化权限显示
- 知识库API区分:管理员调用getDatabases,普通用户调用getAccessibleDatabases - UI权限控制:隐藏普通用户的顶部导航栏、知识库管理、任务中心等管理员功能 - 路由调整:普通用户可访问/agent页面,移除requiresAdmin限制 - 代码优化:删除AgentSingleView.vue,合并到AgentView.vue统一处理 - 错误处理增强:AppLayout中增加配置和知识库加载的try-catch
This commit is contained in:
parent
3eac205c0e
commit
1af5b33ccc
@ -1,4 +1,4 @@
|
||||
import { apiAdminGet, apiAdminPost, apiAdminPut, apiAdminDelete, apiRequest } from './base'
|
||||
import { apiGet, apiAdminGet, apiAdminPost, apiAdminPut, apiAdminDelete, apiRequest } from './base'
|
||||
|
||||
/**
|
||||
* 知识库管理API模块
|
||||
@ -75,7 +75,7 @@ export const databaseApi = {
|
||||
* @returns {Promise} - 可访问的知识库列表
|
||||
*/
|
||||
getAccessibleDatabases: async () => {
|
||||
return apiAdminGet('/api/knowledge/databases/accessible')
|
||||
return apiGet('/api/knowledge/databases/accessible')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -27,10 +27,22 @@
|
||||
<div class="chat-header">
|
||||
<div class="header__left">
|
||||
<slot name="header-left" class="nav-btn"></slot>
|
||||
<div
|
||||
v-if="!chatUIStore.isSidebarOpen && !userStore.isAdmin"
|
||||
type="button"
|
||||
class="sidebar-logo-toggle"
|
||||
@click="toggleSidebar"
|
||||
>
|
||||
<img v-if="sidebarLogo" :src="sidebarLogo" alt="logo" class="sidebar-logo-image" />
|
||||
<PanelLeftOpen v-else class="sidebar-logo-fallback" size="18" />
|
||||
<div class="sidebar-expand-overlay">
|
||||
<PanelLeftOpen class="nav-btn-icon" size="18" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
type="button"
|
||||
class="agent-nav-btn"
|
||||
v-if="!chatUIStore.isSidebarOpen"
|
||||
v-if="!chatUIStore.isSidebarOpen && userStore.isAdmin"
|
||||
@click="toggleSidebar"
|
||||
>
|
||||
<PanelLeftOpen class="nav-btn-icon" size="18" />
|
||||
@ -52,6 +64,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="header__right">
|
||||
<UserInfoComponent v-if="!userStore.isAdmin" />
|
||||
<!-- AgentState 显示按钮已移动到输入框底部 -->
|
||||
<slot name="header-right"></slot>
|
||||
</div>
|
||||
@ -219,6 +232,8 @@ import { ScrollController } from '@/utils/scrollController'
|
||||
import { AgentValidator } from '@/utils/agentValidator'
|
||||
import { useAgentStore } from '@/stores/agent'
|
||||
import { useChatUIStore } from '@/stores/chatUI'
|
||||
import { useInfoStore } from '@/stores/info'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { MessageProcessor } from '@/utils/messageProcessor'
|
||||
import { agentApi, threadApi } from '@/apis'
|
||||
@ -226,6 +241,7 @@ import HumanApprovalModal from '@/components/HumanApprovalModal.vue'
|
||||
import { useApproval } from '@/composables/useApproval'
|
||||
import { useAgentStreamHandler } from '@/composables/useAgentStreamHandler'
|
||||
import AgentPanel from '@/components/AgentPanel.vue'
|
||||
import UserInfoComponent from '@/components/UserInfoComponent.vue'
|
||||
|
||||
// ==================== PROPS & EMITS ====================
|
||||
const props = defineProps({
|
||||
@ -236,6 +252,8 @@ const props = defineProps({
|
||||
// ==================== STORE MANAGEMENT ====================
|
||||
const agentStore = useAgentStore()
|
||||
const chatUIStore = useChatUIStore()
|
||||
const infoStore = useInfoStore()
|
||||
const userStore = useUserStore()
|
||||
const {
|
||||
agents,
|
||||
selectedAgentId,
|
||||
@ -247,9 +265,11 @@ const {
|
||||
availableMcps,
|
||||
availableSkills
|
||||
} = storeToRefs(agentStore)
|
||||
const { organization } = storeToRefs(infoStore)
|
||||
|
||||
// ==================== LOCAL CHAT & UI STATE ====================
|
||||
const userInput = ref('')
|
||||
const sidebarLogo = computed(() => organization.value?.logo || organization.value?.avatar || '')
|
||||
const useRunsApi =
|
||||
import.meta.env.VITE_USE_RUNS_API === 'true' &&
|
||||
localStorage.getItem('force_legacy_stream') !== 'true'
|
||||
@ -1910,6 +1930,7 @@ watch(
|
||||
.header__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.switch-icon {
|
||||
@ -1920,6 +1941,48 @@ watch(
|
||||
.agent-nav-btn:hover .switch-icon {
|
||||
color: var(--main-500);
|
||||
}
|
||||
|
||||
.sidebar-logo-toggle {
|
||||
position: relative;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--gray-150);
|
||||
background: var(--gray-0);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-logo-image {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.sidebar-logo-fallback {
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.sidebar-expand-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--gray-100);
|
||||
color: var(--gray-900);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-logo-toggle:hover .sidebar-expand-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,15 @@
|
||||
>
|
||||
<div class="sidebar-content">
|
||||
<div class="sidebar-header">
|
||||
<div class="header-title">{{ branding.name }}</div>
|
||||
<div class="header-brand">
|
||||
<img
|
||||
v-if="!userStore.isAdmin && (organization.logo || organization.avatar)"
|
||||
:src="organization.logo || organization.avatar"
|
||||
alt="logo"
|
||||
class="brand-logo"
|
||||
/>
|
||||
<div class="header-title">{{ branding.name || organization.name || 'Yuxi-Know' }}</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div
|
||||
class="toggle-sidebar nav-btn"
|
||||
@ -96,7 +104,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, h, ref } from 'vue'
|
||||
import { computed, h } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import {
|
||||
PanelLeftClose,
|
||||
@ -111,13 +119,15 @@ import {
|
||||
import dayjs, { parseToShanghai } from '@/utils/time'
|
||||
import { useChatUIStore } from '@/stores/chatUI'
|
||||
import { useInfoStore } from '@/stores/info'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
// 使用 chatUI store
|
||||
const chatUIStore = useChatUIStore()
|
||||
const infoStore = useInfoStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const { branding } = storeToRefs(infoStore)
|
||||
const { branding, organization } = storeToRefs(infoStore)
|
||||
|
||||
const props = defineProps({
|
||||
currentAgentId: {
|
||||
@ -300,6 +310,24 @@ const togglePin = (chatId) => {
|
||||
border-bottom: 1px solid var(--gray-50);
|
||||
flex-shrink: 0;
|
||||
|
||||
.header-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--gray-100);
|
||||
background: var(--gray-0);
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
|
||||
@ -47,12 +47,21 @@ const handleDebugModalClose = () => {
|
||||
showDebugModal.value = false
|
||||
}
|
||||
|
||||
const getRemoteConfig = () => {
|
||||
configStore.refreshConfig()
|
||||
const getRemoteConfig = async () => {
|
||||
if (!userStore.isAdmin) return
|
||||
try {
|
||||
await configStore.refreshConfig()
|
||||
} catch (error) {
|
||||
console.warn('加载系统配置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getRemoteDatabase = () => {
|
||||
databaseStore.loadDatabases()
|
||||
const getRemoteDatabase = async () => {
|
||||
try {
|
||||
await databaseStore.loadDatabases()
|
||||
} catch (error) {
|
||||
console.warn('加载知识库列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch GitHub stars count
|
||||
@ -73,12 +82,14 @@ const fetchGithubStars = async () => {
|
||||
onMounted(async () => {
|
||||
// 加载信息配置
|
||||
await infoStore.loadInfoConfig()
|
||||
// 加载其他配置
|
||||
getRemoteConfig()
|
||||
getRemoteDatabase()
|
||||
fetchGithubStars() // Fetch GitHub stars on mount
|
||||
// 预加载任务数据,确保任务中心打开时有内容
|
||||
taskerStore.loadTasks()
|
||||
// 加载知识库数据(普通用户加载可访问知识库)
|
||||
await getRemoteDatabase()
|
||||
// 仅管理员加载系统配置和任务中心数据
|
||||
if (userStore.isAdmin) {
|
||||
await getRemoteConfig()
|
||||
taskerStore.loadTasks()
|
||||
fetchGithubStars() // Fetch GitHub stars on mount
|
||||
}
|
||||
})
|
||||
|
||||
// 打印当前页面的路由信息,使用 vue3 的 setup composition API
|
||||
@ -95,37 +106,42 @@ const mainList = computed(() => {
|
||||
path: '/agent',
|
||||
icon: Bot,
|
||||
activeIcon: Bot
|
||||
},
|
||||
{
|
||||
name: '图谱',
|
||||
path: '/graph',
|
||||
icon: Waypoints,
|
||||
activeIcon: Waypoints
|
||||
},
|
||||
{
|
||||
name: '知识库',
|
||||
path: '/database',
|
||||
icon: LibraryBig,
|
||||
activeIcon: LibraryBig
|
||||
}
|
||||
]
|
||||
|
||||
if (userStore.isSuperAdmin) {
|
||||
if (userStore.isAdmin) {
|
||||
items.push(
|
||||
{
|
||||
name: '图谱',
|
||||
path: '/graph',
|
||||
icon: Waypoints,
|
||||
activeIcon: Waypoints
|
||||
},
|
||||
{
|
||||
name: '知识库',
|
||||
path: '/database',
|
||||
icon: LibraryBig,
|
||||
activeIcon: LibraryBig
|
||||
}
|
||||
)
|
||||
|
||||
if (userStore.isSuperAdmin) {
|
||||
items.push({
|
||||
name: '扩展管理',
|
||||
path: '/extensions',
|
||||
icon: Blocks,
|
||||
activeIcon: Blocks
|
||||
})
|
||||
}
|
||||
|
||||
items.push({
|
||||
name: '扩展管理',
|
||||
path: '/extensions',
|
||||
icon: Blocks,
|
||||
activeIcon: Blocks
|
||||
name: 'Dashboard',
|
||||
path: '/dashboard',
|
||||
icon: BarChart3,
|
||||
activeIcon: BarChart3
|
||||
})
|
||||
}
|
||||
|
||||
items.push({
|
||||
name: 'Dashboard',
|
||||
path: '/dashboard',
|
||||
icon: BarChart3,
|
||||
activeIcon: BarChart3
|
||||
})
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
@ -137,7 +153,7 @@ provide('settingsModal', {
|
||||
|
||||
<template>
|
||||
<div class="app-layout" :class="{ 'use-top-bar': layoutSettings.useTopBar }">
|
||||
<div class="header" :class="{ 'top-bar': layoutSettings.useTopBar }">
|
||||
<div v-if="userStore.isAdmin" class="header" :class="{ 'top-bar': layoutSettings.useTopBar }">
|
||||
<div class="logo circle">
|
||||
<router-link to="/">
|
||||
<img :src="infoStore.organization.avatar" />
|
||||
@ -163,6 +179,7 @@ provide('settingsModal', {
|
||||
</a-tooltip>
|
||||
</RouterLink>
|
||||
<div
|
||||
v-if="userStore.isAdmin"
|
||||
class="nav-item task-center"
|
||||
:class="{ active: isDrawerOpen }"
|
||||
@click="taskerStore.openDrawer()"
|
||||
@ -217,7 +234,7 @@ provide('settingsModal', {
|
||||
>
|
||||
<DebugComponent />
|
||||
</a-modal>
|
||||
<TaskCenterDrawer />
|
||||
<TaskCenterDrawer v-if="userStore.isAdmin" />
|
||||
<SettingsModal v-model:visible="showSettingsModal" @close="() => (showSettingsModal = false)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -35,16 +35,16 @@ const router = createRouter({
|
||||
path: '',
|
||||
name: 'AgentComp',
|
||||
component: () => import('../views/AgentView.vue'),
|
||||
meta: { keepAlive: true, requiresAuth: true, requiresAdmin: true }
|
||||
meta: { keepAlive: true, requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: ':agent_id',
|
||||
name: 'AgentCompWithId',
|
||||
component: () => import('../views/AgentView.vue'),
|
||||
meta: { keepAlive: true, requiresAuth: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/agent/:agent_id',
|
||||
name: 'AgentSinglePage',
|
||||
component: () => import('../views/AgentSingleView.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/graph',
|
||||
name: 'graph',
|
||||
@ -173,13 +173,13 @@ router.beforeEach(async (to, from, next) => {
|
||||
if (agentIds.length > 0) {
|
||||
next(`/agent/${agentIds[0]}`)
|
||||
} else {
|
||||
// 没有可用的智能体,跳转到首页
|
||||
next('/')
|
||||
// 没有可用的智能体,跳转到聊天页
|
||||
next('/agent')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取智能体信息失败:', error)
|
||||
next('/')
|
||||
next('/agent')
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -195,11 +195,11 @@ router.beforeEach(async (to, from, next) => {
|
||||
if (defaultAgent && defaultAgent.id) {
|
||||
next(`/agent/${defaultAgent.id}`)
|
||||
} else {
|
||||
next('/')
|
||||
next('/agent')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取智能体信息失败:', error)
|
||||
next('/')
|
||||
next('/agent')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -39,11 +39,11 @@ export const useConfigStore = defineStore('config', () => {
|
||||
})
|
||||
}
|
||||
|
||||
function refreshConfig() {
|
||||
configApi.getConfig().then((data) => {
|
||||
console.log('config', data)
|
||||
setConfig(data)
|
||||
})
|
||||
async function refreshConfig() {
|
||||
const data = await configApi.getConfig()
|
||||
console.log('config', data)
|
||||
setConfig(data)
|
||||
return data
|
||||
}
|
||||
|
||||
return { config, setConfig, setConfigValue, refreshConfig, setConfigValues }
|
||||
|
||||
@ -3,12 +3,14 @@ import { ref, reactive } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { databaseApi, documentApi, queryApi } from '@/apis/knowledge_api'
|
||||
import { useTaskerStore } from '@/stores/tasker'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { parseToShanghai } from '@/utils/time'
|
||||
|
||||
export const useDatabaseStore = defineStore('database', () => {
|
||||
const router = useRouter()
|
||||
const taskerStore = useTaskerStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// State
|
||||
const databases = ref([])
|
||||
@ -41,11 +43,15 @@ export const useDatabaseStore = defineStore('database', () => {
|
||||
let autoRefreshManualOverride = false // Indicates user explicitly disabled auto-refresh
|
||||
|
||||
// Actions
|
||||
// 管理员获取所有知识库,普通用户获取有权限访问的知识库
|
||||
async function loadDatabases() {
|
||||
state.listLoading = true
|
||||
try {
|
||||
const data = await databaseApi.getDatabases()
|
||||
databases.value = data.databases.sort((a, b) => {
|
||||
const data = userStore.isAdmin
|
||||
? await databaseApi.getDatabases()
|
||||
: await databaseApi.getAccessibleDatabases()
|
||||
const list = data?.databases || []
|
||||
databases.value = list.sort((a, b) => {
|
||||
const timeA = parseToShanghai(a.created_at)
|
||||
const timeB = parseToShanghai(b.created_at)
|
||||
if (!timeA && !timeB) return 0
|
||||
|
||||
@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { taskerApi } from '@/apis/tasker'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { parseToShanghai } from '@/utils/time'
|
||||
|
||||
const ACTIVE_STATUSES = new Set(['pending', 'running', 'queued'])
|
||||
@ -32,6 +33,7 @@ const toTask = (raw = {}) => ({
|
||||
})
|
||||
|
||||
export const useTaskerStore = defineStore('tasker', () => {
|
||||
const userStore = useUserStore()
|
||||
const tasks = ref([])
|
||||
const loading = ref(false)
|
||||
const lastError = ref(null)
|
||||
@ -80,6 +82,13 @@ export const useTaskerStore = defineStore('tasker', () => {
|
||||
}
|
||||
|
||||
async function loadTasks(params = {}) {
|
||||
if (!userStore.isAdmin) {
|
||||
tasks.value = []
|
||||
summary.value = createDefaultSummary()
|
||||
lastError.value = null
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
lastError.value = null
|
||||
try {
|
||||
|
||||
@ -1,376 +0,0 @@
|
||||
<template>
|
||||
<div class="agent-single-view">
|
||||
<!-- 智能体选择弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="agentModalOpen"
|
||||
title="选择智能体"
|
||||
:width="800"
|
||||
:footer="null"
|
||||
:maskClosable="true"
|
||||
class="agent-modal"
|
||||
>
|
||||
<div class="agent-modal-content">
|
||||
<div class="agents-grid">
|
||||
<div
|
||||
v-for="agent in agents"
|
||||
:key="agent.id"
|
||||
class="agent-card"
|
||||
:class="{ selected: agent.id === agentId }"
|
||||
@click="selectAgentFromModal(agent.id)"
|
||||
>
|
||||
<div class="agent-card-header">
|
||||
<div class="agent-card-title">
|
||||
<span class="agent-card-name">{{ agent.name || 'Unknown' }}</span>
|
||||
</div>
|
||||
<StarFilled v-if="agent.id === defaultAgentId" class="default-icon" />
|
||||
<StarOutlined
|
||||
v-else
|
||||
@click.prevent="setAsDefaultAgent(agent.id)"
|
||||
class="default-icon"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="agent-card-description">
|
||||
{{ agent.description || '' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 智能体聊天界面 -->
|
||||
<AgentChatComponent ref="chatComponentRef" :agent-id="agentId" :single-mode="true">
|
||||
<template #header-left>
|
||||
<div type="button" class="agent-nav-btn" @click="openAgentModal">
|
||||
<span class="text">{{ currentAgentName || '选择智能体' }}</span>
|
||||
<ChevronDown size="16" class="switch-icon" />
|
||||
</div>
|
||||
</template>
|
||||
<template #header-right>
|
||||
<div type="button" class="agent-nav-btn" @click="handleShareChat">
|
||||
<Share2 size="18" class="nav-btn-icon" />
|
||||
<span class="text">分享</span>
|
||||
</div>
|
||||
<UserInfoComponent />
|
||||
</template>
|
||||
</AgentChatComponent>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Share2, ChevronDown } from 'lucide-vue-next'
|
||||
import { StarFilled, StarOutlined } from '@ant-design/icons-vue'
|
||||
import AgentChatComponent from '@/components/AgentChatComponent.vue'
|
||||
import UserInfoComponent from '@/components/UserInfoComponent.vue'
|
||||
import { ChatExporter } from '@/utils/chatExporter'
|
||||
import { handleChatError } from '@/utils/errorHandler'
|
||||
import { useAgentStore } from '@/stores/agent'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const agentStore = useAgentStore()
|
||||
|
||||
const agentId = computed(() => route.params.agent_id)
|
||||
const chatComponentRef = ref(null)
|
||||
|
||||
// 智能体选择弹窗状态
|
||||
const agentModalOpen = ref(false)
|
||||
|
||||
// 从 store 获取智能体数据
|
||||
const { agents, defaultAgentId } = storeToRefs(agentStore)
|
||||
|
||||
// 当前智能体名称
|
||||
const currentAgentName = computed(() => {
|
||||
if (!agentId.value || !agents.value?.length) return '智能体加载中……'
|
||||
const agent = agents.value.find((a) => a.id === agentId.value)
|
||||
return agent ? agent.name : '未知智能体'
|
||||
})
|
||||
|
||||
// 打开智能体选择弹窗
|
||||
const openAgentModal = () => {
|
||||
agentModalOpen.value = true
|
||||
}
|
||||
|
||||
// 从弹窗中选择智能体 - 切换路由
|
||||
const selectAgentFromModal = (newAgentId) => {
|
||||
if (newAgentId === agentId.value) {
|
||||
// 如果选择的是当前智能体,只需要关闭弹窗
|
||||
agentModalOpen.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 跳转到新的智能体页面
|
||||
router.push(`/agent/${newAgentId}`)
|
||||
agentModalOpen.value = false
|
||||
}
|
||||
|
||||
// 设置默认智能体
|
||||
const setAsDefaultAgent = async (agentIdToSet) => {
|
||||
try {
|
||||
await agentStore.setDefaultAgent(agentIdToSet)
|
||||
message.success('已设置为默认智能体')
|
||||
} catch (error) {
|
||||
handleChatError(error, 'save')
|
||||
}
|
||||
}
|
||||
|
||||
const handleShareChat = async () => {
|
||||
try {
|
||||
const exportData = chatComponentRef.value?.getExportPayload?.()
|
||||
|
||||
if (!exportData) {
|
||||
message.warning('当前没有可导出的对话内容')
|
||||
return
|
||||
}
|
||||
|
||||
const hasMessages = Boolean(exportData.messages?.length)
|
||||
const hasOngoingMessages = Boolean(exportData.onGoingMessages?.length)
|
||||
|
||||
if (!hasMessages && !hasOngoingMessages) {
|
||||
message.warning('当前对话暂无内容可导出,请先进行对话')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await ChatExporter.exportToHTML(exportData)
|
||||
message.success(`对话已导出为HTML文件: ${result.filename}`)
|
||||
} catch (error) {
|
||||
if (error?.message?.includes('没有可导出的对话内容')) {
|
||||
message.warning('当前对话暂无内容可导出,请先进行对话')
|
||||
return
|
||||
}
|
||||
handleChatError(error, 'export')
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化时确保智能体 store 已加载
|
||||
onMounted(async () => {
|
||||
if (!agentStore.isInitialized) {
|
||||
try {
|
||||
await agentStore.initialize()
|
||||
} catch (error) {
|
||||
console.error('初始化智能体 store 失败:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.agent-single-view {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.user-info-wrapper {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 20px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
// 智能体选择弹窗样式
|
||||
.agent-modal {
|
||||
:deep(.ant-modal-content) {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.ant-modal-header) {
|
||||
background: var(--gray-0);
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
padding: 16px 20px;
|
||||
|
||||
.ant-modal-title {
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-modal-body) {
|
||||
padding: 20px;
|
||||
background: var(--gray-0);
|
||||
}
|
||||
|
||||
.agent-modal-content {
|
||||
.agents-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 12px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.agent-card {
|
||||
border: 1px solid var(--gray-200);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background: var(--gray-0);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--main-color);
|
||||
}
|
||||
|
||||
.agent-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.agent-card-title {
|
||||
flex: 1;
|
||||
|
||||
.agent-card-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.default-icon {
|
||||
color: var(--color-warning-500);
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-warning-600);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.agent-card-description {
|
||||
font-size: 14px;
|
||||
color: var(--gray-700);
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-color: var(--main-color);
|
||||
background: var(--main-20);
|
||||
|
||||
.agent-card-header .agent-card-title .agent-card-name {
|
||||
color: var(--main-color);
|
||||
}
|
||||
|
||||
.agent-card-description {
|
||||
color: var(--gray-900);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式适配智能体弹窗
|
||||
@media (max-width: 768px) {
|
||||
.agent-modal {
|
||||
.agent-modal-content {
|
||||
.agents-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 侧边栏样式
|
||||
.sidebar {
|
||||
// position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 240px;
|
||||
background-color: var(--gray-50);
|
||||
transition: all 0.3s ease;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
|
||||
&.collapsed {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
flex: 1;
|
||||
padding: 20px 10px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.user-icon {
|
||||
cursor: pointer;
|
||||
margin-bottom: 20px;
|
||||
padding-left: 4px 8px;
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
position: absolute;
|
||||
right: -15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background-color: var(--gray-0);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.agent-nav-btn {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 6px;
|
||||
color: var(--gray-900);
|
||||
cursor: pointer;
|
||||
width: auto;
|
||||
font-size: 15px;
|
||||
transition: background-color 0.3s;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--gray-50);
|
||||
}
|
||||
|
||||
.nav-btn-icon {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.switch-icon {
|
||||
color: var(--gray-500);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover .switch-icon {
|
||||
color: var(--main-500);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -82,7 +82,7 @@
|
||||
</a-dropdown>
|
||||
</template>
|
||||
|
||||
<template #header-right>
|
||||
<template #header-right v-if="userStore.isAdmin">
|
||||
<div
|
||||
v-if="selectedAgentId"
|
||||
ref="moreButtonRef"
|
||||
@ -103,13 +103,13 @@
|
||||
/>
|
||||
|
||||
<!-- 反馈模态框 -->
|
||||
<FeedbackModalComponent ref="feedbackModal" :agent-id="selectedAgentId" />
|
||||
<FeedbackModalComponent v-if="userStore.isAdmin" ref="feedbackModal" :agent-id="selectedAgentId" />
|
||||
|
||||
<!-- 自定义更多菜单 -->
|
||||
<Teleport to="body">
|
||||
<Transition name="menu-fade">
|
||||
<div
|
||||
v-if="chatUIStore.moreMenuOpen"
|
||||
v-if="userStore.isAdmin && chatUIStore.moreMenuOpen"
|
||||
ref="moreMenuRef"
|
||||
class="more-popup-menu"
|
||||
:style="{
|
||||
@ -125,10 +125,6 @@
|
||||
<MessageOutlined class="menu-icon" />
|
||||
<span class="menu-text">查看反馈</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="handlePreview">
|
||||
<EyeOutlined class="menu-icon" />
|
||||
<span class="menu-text">预览页面</span>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
@ -137,10 +133,11 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { MessageOutlined, ShareAltOutlined, EyeOutlined } from '@ant-design/icons-vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import { MessageOutlined, ShareAltOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { Settings2, Ellipsis, ChevronDown, Star, CirclePlus, SquarePen } from 'lucide-vue-next'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import AgentChatComponent from '@/components/AgentChatComponent.vue'
|
||||
import AgentConfigSidebar from '@/components/AgentConfigSidebar.vue'
|
||||
import FeedbackModalComponent from '@/components/dashboard/FeedbackModalComponent.vue'
|
||||
@ -162,12 +159,65 @@ const configDropdownOpen = ref(false)
|
||||
const userStore = useUserStore()
|
||||
const agentStore = useAgentStore()
|
||||
const chatUIStore = useChatUIStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 从 agentStore 中获取响应式状态
|
||||
const { selectedAgentId, agentConfigs, selectedAgentConfigId, selectedConfigSummary } =
|
||||
const { agents, selectedAgentId, agentConfigs, selectedAgentConfigId, selectedConfigSummary } =
|
||||
storeToRefs(agentStore)
|
||||
|
||||
const syncingRouteAgent = ref(false)
|
||||
|
||||
const getRouteAgentId = () => {
|
||||
const value = route.params.agent_id
|
||||
return typeof value === 'string' ? value : ''
|
||||
}
|
||||
|
||||
const syncSelectedAgentFromRoute = async () => {
|
||||
const routeAgentId = getRouteAgentId()
|
||||
if (!routeAgentId) return
|
||||
|
||||
syncingRouteAgent.value = true
|
||||
try {
|
||||
if (!agentStore.isInitialized) {
|
||||
await agentStore.initialize()
|
||||
}
|
||||
|
||||
const routeAgentExists = (agents.value || []).some((agent) => agent.id === routeAgentId)
|
||||
if (!routeAgentExists) {
|
||||
if (selectedAgentId.value) {
|
||||
await router.replace({ name: 'AgentCompWithId', params: { agent_id: selectedAgentId.value } })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedAgentId.value !== routeAgentId) {
|
||||
await agentStore.selectAgent(routeAgentId)
|
||||
}
|
||||
} catch (error) {
|
||||
handleChatError(error, 'load')
|
||||
} finally {
|
||||
syncingRouteAgent.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.params.agent_id,
|
||||
() => {
|
||||
syncSelectedAgentFromRoute()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(selectedAgentId, (newAgentId) => {
|
||||
if (!newAgentId || syncingRouteAgent.value) return
|
||||
const routeAgentId = getRouteAgentId()
|
||||
if (routeAgentId === newAgentId) return
|
||||
router.replace({ name: 'AgentCompWithId', params: { agent_id: newAgentId } })
|
||||
})
|
||||
|
||||
const openConfigSidebar = () => {
|
||||
configDropdownOpen.value = false
|
||||
chatUIStore.isConfigSidebarOpen = true
|
||||
}
|
||||
|
||||
@ -176,6 +226,7 @@ const createConfigLoading = ref(false)
|
||||
const createConfigName = ref('')
|
||||
|
||||
const openCreateConfigModal = () => {
|
||||
configDropdownOpen.value = false
|
||||
createConfigName.value = ''
|
||||
createConfigModalOpen.value = true
|
||||
}
|
||||
@ -288,13 +339,6 @@ const handleFeedback = () => {
|
||||
closeMoreMenu()
|
||||
feedbackModal.value?.show()
|
||||
}
|
||||
|
||||
const handlePreview = () => {
|
||||
closeMoreMenu()
|
||||
if (selectedAgentId.value) {
|
||||
window.open(`/agent/${selectedAgentId.value}`, '_blank')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@ -414,37 +414,13 @@ const handleLogin = async () => {
|
||||
|
||||
// 根据用户角色决定重定向目标
|
||||
if (redirectPath === '/') {
|
||||
// 如果是管理员,直接跳转到/chat页面
|
||||
if (userStore.isAdmin) {
|
||||
// 统一跳转到聊天页面(管理员与普通用户共享同一聊天界面)
|
||||
try {
|
||||
await agentStore.initialize()
|
||||
router.push('/agent')
|
||||
return
|
||||
}
|
||||
|
||||
// 普通用户跳转到默认智能体
|
||||
try {
|
||||
// 初始化agentStore并获取智能体信息
|
||||
await agentStore.initialize()
|
||||
|
||||
// 尝试获取默认智能体
|
||||
if (agentStore.defaultAgentId) {
|
||||
// 如果存在默认智能体,直接跳转
|
||||
router.push(`/agent/${agentStore.defaultAgentId}`)
|
||||
return
|
||||
}
|
||||
|
||||
// 没有默认智能体,获取第一个可用智能体
|
||||
const agentIds = Object.keys(agentStore.agents)
|
||||
if (agentIds.length > 0) {
|
||||
router.push(`/agent/${agentIds[0]}`)
|
||||
return
|
||||
}
|
||||
|
||||
// 没有可用智能体,回退到首页
|
||||
router.push('/')
|
||||
} catch (error) {
|
||||
console.error('获取智能体信息失败:', error)
|
||||
router.push('/')
|
||||
router.push('/agent')
|
||||
}
|
||||
} else {
|
||||
// 跳转到其他预设的路径
|
||||
|
||||
Loading…
Reference in New Issue
Block a user