From df8467bc8891af847802cc6aee501c18d50f451a Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期二, 31 三月 2026 16:32:26 +0800
Subject: [PATCH] #前端
---
rsf-design/src/views/system/ai-mcp-mount/index.vue | 64 +
rsf-design/src/api/ai-chat.js | 153 +++++
rsf-design/src/plugins/iconify.collections.js | 12
rsf-design/src/views/manager/loc-preview/modules/loc-preview-detail-drawer.vue | 4
rsf-design/src/views/abnormal/index.vue | 42 +
rsf-design/src/components/core/layouts/art-chat-window/index.vue | 1218 +++++++++++++++++++++++++++++++++++------
rsf-design/src/locales/langs/en.json | 85 ++
rsf-design/src/views/system/role/rolePage.helpers.js | 38
rsf-design/src/locales/langs/zh.json | 85 ++
rsf-design/src/views/system/role/index.vue | 24
10 files changed, 1,466 insertions(+), 259 deletions(-)
diff --git a/rsf-design/src/api/ai-chat.js b/rsf-design/src/api/ai-chat.js
new file mode 100644
index 0000000..5b6b8df
--- /dev/null
+++ b/rsf-design/src/api/ai-chat.js
@@ -0,0 +1,153 @@
+import request from '@/utils/http'
+import { useUserStore } from '@/store/modules/user'
+
+const DEFAULT_PROMPT_CODE = 'home.default'
+
+function buildRuntimeParams(promptCode = DEFAULT_PROMPT_CODE, sessionId = null, aiParamId = null) {
+ return {
+ promptCode,
+ ...(sessionId ? { sessionId } : {}),
+ ...(aiParamId !== null && aiParamId !== undefined ? { aiParamId } : {})
+ }
+}
+
+function fetchGetAiRuntime(promptCode = DEFAULT_PROMPT_CODE, sessionId = null, aiParamId = null) {
+ return request.get({
+ url: '/ai/chat/runtime',
+ params: buildRuntimeParams(promptCode, sessionId, aiParamId),
+ showErrorMessage: false
+ })
+}
+
+function fetchGetAiSessions(promptCode = DEFAULT_PROMPT_CODE, keyword = '') {
+ return request.get({
+ url: '/ai/chat/sessions',
+ params: {
+ promptCode,
+ ...(keyword ? { keyword } : {})
+ },
+ showErrorMessage: false
+ })
+}
+
+function fetchRemoveAiSession(sessionId) {
+ return request.post({
+ url: `/ai/chat/session/remove/${sessionId}`,
+ showErrorMessage: false
+ })
+}
+
+function fetchRenameAiSession(sessionId, title) {
+ return request.post({
+ url: `/ai/chat/session/rename/${sessionId}`,
+ params: { title },
+ showErrorMessage: false
+ })
+}
+
+function fetchPinAiSession(sessionId, pinned) {
+ return request.post({
+ url: `/ai/chat/session/pin/${sessionId}`,
+ params: { pinned },
+ showErrorMessage: false
+ })
+}
+
+function fetchClearAiSessionMemory(sessionId) {
+ return request.post({
+ url: `/ai/chat/session/memory/clear/${sessionId}`,
+ showErrorMessage: false
+ })
+}
+
+function fetchRetainAiSessionLatestRound(sessionId) {
+ return request.post({
+ url: `/ai/chat/session/memory/retain-latest/${sessionId}`,
+ showErrorMessage: false
+ })
+}
+
+async function fetchStreamAiChat(payload, { signal, onEvent } = {}) {
+ const { VITE_API_URL } = import.meta.env
+ const { accessToken } = useUserStore()
+ const response = await fetch(`${VITE_API_URL}/ai/chat/stream`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'text/event-stream',
+ ...(accessToken ? { Authorization: accessToken } : {})
+ },
+ body: JSON.stringify(payload),
+ signal
+ })
+
+ if (!response.ok) {
+ throw new Error(`AI 璇锋眰澶辫触 (${response.status})`)
+ }
+
+ if (!response.body) {
+ throw new Error('AI 鍝嶅簲娴佷笉鍙敤')
+ }
+
+ const reader = response.body.getReader()
+ const decoder = new TextDecoder('utf-8')
+ let buffer = ''
+
+ while (true) {
+ const { done, value } = await reader.read()
+ if (done) {
+ break
+ }
+ buffer += decoder.decode(value, { stream: true })
+ const events = buffer.split(/\r?\n\r?\n/)
+ buffer = events.pop() || ''
+ events.forEach((item) => dispatchSseEvent(item, onEvent))
+ }
+
+ if (buffer.trim()) {
+ dispatchSseEvent(buffer, onEvent)
+ }
+}
+
+function dispatchSseEvent(rawEvent, onEvent) {
+ if (!onEvent) {
+ return
+ }
+
+ const lines = rawEvent.split(/\r?\n/)
+ let eventName = 'message'
+ const dataLines = []
+
+ lines.forEach((line) => {
+ if (line.startsWith('event:')) {
+ eventName = line.slice(6).trim()
+ }
+ if (line.startsWith('data:')) {
+ dataLines.push(line.slice(5).trim())
+ }
+ })
+
+ if (!dataLines.length) {
+ return
+ }
+
+ const rawData = dataLines.join('\n')
+ let payload = rawData
+ try {
+ payload = JSON.parse(rawData)
+ } catch {
+ }
+ onEvent(eventName, payload)
+}
+
+export {
+ DEFAULT_PROMPT_CODE,
+ fetchClearAiSessionMemory,
+ fetchGetAiRuntime,
+ fetchGetAiSessions,
+ fetchPinAiSession,
+ fetchRemoveAiSession,
+ fetchRenameAiSession,
+ fetchRetainAiSessionLatestRound,
+ fetchStreamAiChat
+}
diff --git a/rsf-design/src/components/core/layouts/art-chat-window/index.vue b/rsf-design/src/components/core/layouts/art-chat-window/index.vue
index 01d78c8..10efb89 100644
--- a/rsf-design/src/components/core/layouts/art-chat-window/index.vue
+++ b/rsf-design/src/components/core/layouts/art-chat-window/index.vue
@@ -1,228 +1,1056 @@
-<!-- 绯荤粺鑱婂ぉ绐楀彛 -->
<template>
- <div>
- <ElDrawer v-model="isDrawerVisible" :size="isMobile ? '100%' : '480px'" :with-header="false">
- <div class="mb-5 flex-cb">
- <div>
- <span class="text-base font-medium">Art Bot</span>
- <div class="mt-1.5 flex-c gap-1">
- <div
- class="h-2 w-2 rounded-full"
- :class="isOnline ? 'bg-success/100' : 'bg-danger/100'"
- ></div>
- <span class="text-xs text-g-600">{{ isOnline ? '鍦ㄧ嚎' : '绂荤嚎' }}</span>
+ <ElDrawer
+ v-model="isDrawerVisible"
+ :size="isMobile ? '100%' : '76vw'"
+ :with-header="false"
+ destroy-on-close
+ class="ai-chat-drawer"
+ >
+ <div class="flex h-full min-h-0 flex-col overflow-hidden bg-[var(--art-main-bg-color)]">
+ <div class="flex items-center gap-4 border-b border-[var(--el-border-color-lighter)] bg-[var(--art-main-bg-color)] px-6 py-4">
+ <div class="flex size-11 items-center justify-center rounded-3xl bg-[var(--el-color-primary-light-9)] text-[var(--el-color-primary)]">
+ <ArtSvgIcon icon="ri:robot-2-line" class="text-[22px]" />
+ </div>
+ <div class="min-w-0 flex-1">
+ <div class="flex flex-wrap items-center gap-2">
+ <h3 class="text-base font-semibold text-[var(--art-gray-900)]">{{ $t('ai.drawer.title') }}</h3>
+ <ElTag v-if="streaming" type="success" effect="light" round>Streaming</ElTag>
</div>
+ <p class="mt-1 truncate text-xs text-[var(--art-gray-500)]">
+ {{ runtime?.promptName || runtime?.promptCode || DEFAULT_PROMPT_CODE }}
+ </p>
</div>
- <div>
- <ElIcon class="c-p" :size="20" @click="closeChat">
- <Close />
- </ElIcon>
- </div>
+ <ElButton plain :disabled="streaming" @click="startNewSession">
+ <ArtSvgIcon icon="ri:add-line" class="mr-1 text-sm" />
+ {{ $t('ai.drawer.newSession') }}
+ </ElButton>
+ <ArtIconButton icon="ri:close-line" @click="closeChat" />
</div>
- <div class="flex h-[calc(100%-70px)] flex-col">
- <!-- 鑱婂ぉ娑堟伅鍖哄煙 -->
- <div
- class="flex-1 overflow-y-auto border-t-d px-4 py-7.5 [&::-webkit-scrollbar]:!w-1"
- ref="messageContainer"
- >
- <template v-for="(message, index) in messages" :key="index">
- <div
- :class="[
- 'mb-7.5 flex w-full items-start gap-2',
- message.isMe ? 'flex-row-reverse' : 'flex-row'
- ]"
+
+ <div class="min-h-0 flex-1 bg-g-100/35 ai-chat-body">
+ <aside class="box-border flex min-h-0 flex-col gap-4 p-4 ai-chat-sidebar">
+ <div class="rounded-3xl bg-[var(--art-main-bg-color)] p-4 shadow-[0_12px_36px_rgba(15,23,42,0.05)] ring-1 ring-[var(--el-border-color-lighter)]">
+ <div class="mb-3 flex items-center justify-between gap-3">
+ <div class="text-sm font-semibold text-[var(--art-gray-900)]">{{ $t('ai.drawer.sessionList') }}</div>
+ <ElTag effect="plain" round>{{ sessions.length }}</ElTag>
+ </div>
+ <ElInput
+ v-model="sessionKeyword"
+ clearable
+ :placeholder="$t('ai.drawer.searchPlaceholder')"
>
- <ElAvatar :size="32" :src="message.avatar" class="shrink-0" />
+ <template #prefix>
+ <ArtSvgIcon icon="ri:search-line" />
+ </template>
+ </ElInput>
+ </div>
+
+ <div class="min-h-0 flex-1 overflow-hidden rounded-3xl bg-[var(--art-main-bg-color)] shadow-[0_12px_36px_rgba(15,23,42,0.05)] ring-1 ring-[var(--el-border-color-lighter)]">
+ <ElScrollbar class="h-full px-3 py-3">
+ <div class="space-y-3">
+ <ElEmpty v-if="!sessions.length" :description="$t('ai.drawer.noSessions')" :image-size="88" />
+
+ <button
+ v-for="item in sessions"
+ :key="item.sessionId"
+ type="button"
+ class="w-full rounded-3xl border p-3 text-left transition-all"
+ :class="
+ item.sessionId === sessionId
+ ? 'border-[var(--el-color-primary-light-7)] bg-[var(--el-color-primary-light-9)] shadow-[0_12px_30px_rgba(64,158,255,0.08)]'
+ : 'border-transparent bg-[var(--art-main-bg-color)] hover:border-[var(--el-color-primary-light-8)] hover:bg-g-100/50'
+ "
+ :disabled="streaming"
+ @click="handleSwitchSession(item.sessionId)"
+ >
+ <div class="flex items-start gap-3">
+ <div
+ class="mt-0.5 flex size-9 shrink-0 items-center justify-center rounded-2xl"
+ :class="
+ item.sessionId === sessionId
+ ? 'bg-[var(--el-color-primary)]/12 text-[var(--el-color-primary)]'
+ : 'bg-g-100 text-[var(--art-gray-500)]'
+ "
+ >
+ <ArtSvgIcon :icon="item.pinned ? 'ri:pushpin-2-fill' : 'ri:chat-3-line'" class="text-base" />
+ </div>
+
+ <div class="min-w-0 flex-1">
+ <div class="flex items-start justify-between gap-2">
+ <div class="min-w-0">
+ <div class="truncate text-sm font-medium text-[var(--art-gray-900)]">
+ {{ item.title || $t('ai.drawer.sessionTitle', { id: item.sessionId }) }}
+ </div>
+ <div class="mt-1 truncate text-xs text-[var(--art-gray-500)]">
+ {{ item.lastMessageTime || $t('ai.drawer.sessionMetric', { id: item.sessionId }) }}
+ </div>
+ </div>
+ <div class="shrink-0" @click.stop>
+ <ArtButtonMore
+ :list="getSessionActions(item)"
+ @click="handleSessionAction(item, $event)"
+ />
+ </div>
+ </div>
+
+ <div class="mt-3 line-clamp-2 text-xs leading-5 text-[var(--art-gray-500)]">
+ {{ item.lastMessagePreview || $t('ai.drawer.emptyHint') }}
+ </div>
+ </div>
+ </div>
+ </button>
+ </div>
+ </ElScrollbar>
+ </div>
+ </aside>
+
+ <section class="box-border flex min-h-0 flex-1 flex-col gap-4 p-4 pl-0">
+ <div class="rounded-3xl bg-[var(--art-main-bg-color)] px-5 py-4 shadow-[0_12px_36px_rgba(15,23,42,0.05)] ring-1 ring-[var(--el-border-color-lighter)]">
+ <div class="flex flex-wrap items-center justify-between gap-3">
+ <div>
+ <div class="text-sm font-semibold text-[var(--art-gray-900)]">{{ $t('ai.drawer.runtimeOverview') }}</div>
+ <div class="mt-1 text-xs text-[var(--art-gray-500)]">
+ {{ $t('ai.drawer.modelSelectorHint') }}
+ </div>
+ </div>
+ <ElButton text @click="runtimePanelExpanded = !runtimePanelExpanded">
+ {{ $t(runtimePanelExpanded ? 'ai.drawer.runtimeCollapse' : 'ai.drawer.runtimeExpand') }}
+ </ElButton>
+ </div>
+
+ <ElCollapseTransition>
+ <div v-show="runtimePanelExpanded" class="mt-4 space-y-4">
+ <div class="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
+ <div
+ v-for="item in runtimeMetricCards"
+ :key="item.label"
+ class="rounded-2xl bg-g-100/55 px-4 py-3 ring-1 ring-[var(--el-border-color-extra-light)]"
+ >
+ <div class="mb-2 flex items-center gap-2 text-xs text-[var(--art-gray-500)]">
+ <ArtSvgIcon :icon="item.icon" class="text-sm" />
+ <span>{{ item.label }}</span>
+ </div>
+ <div class="truncate text-sm font-semibold text-[var(--art-gray-900)]">
+ {{ item.value }}
+ </div>
+ </div>
+ </div>
+
+ <div class="flex flex-wrap items-center gap-3">
+ <ElSelect
+ v-if="selectableModelOptions.length"
+ v-model="selectedAiParamId"
+ :placeholder="$t('ai.drawer.modelSelectorLabel')"
+ :disabled="streaming || loadingRuntime || selectableModelOptions.length <= 1"
+ class="min-w-[280px]"
+ @change="handleModelChange"
+ >
+ <ElOption
+ v-for="item in selectableModelOptions"
+ :key="String(item.aiParamId)"
+ :label="formatModelOption(item)"
+ :value="item.aiParamId"
+ />
+ </ElSelect>
+
+ <div class="flex flex-wrap gap-2">
+ <ElButton
+ v-for="item in quickLinks"
+ :key="item.path"
+ plain
+ size="small"
+ @click="navigateTo(item.path)"
+ >
+ {{ item.label }}
+ </ElButton>
+ <ElButton plain size="small" :disabled="!sessionId || streaming" @click="handleRetainLatestRound">
+ {{ $t('ai.drawer.retainLatestRound') }}
+ </ElButton>
+ <ElButton plain size="small" :disabled="!sessionId || streaming" @click="handleClearMemory">
+ {{ $t('ai.drawer.clearMemory') }}
+ </ElButton>
+ </div>
+ </div>
+
+ <div class="flex flex-wrap gap-2">
+ <ElTag effect="plain" round>{{ $t('ai.drawer.requestMetric', { value: runtimeSummary.requestId }) }}</ElTag>
+ <ElTag effect="plain" round>{{ $t('ai.drawer.sessionMetric', { id: sessionId || '--' }) }}</ElTag>
+ <ElTag effect="plain" round>{{ $t('ai.drawer.recentMetric', { value: runtimeSummary.recentMessageCount }) }}</ElTag>
+ <ElTag :type="runtimeSummary.hasSummary ? 'success' : 'info'" effect="light" round>
+ {{ $t(runtimeSummary.hasSummary ? 'ai.drawer.hasSummary' : 'ai.drawer.noSummary') }}
+ </ElTag>
+ <ElTag :type="runtimeSummary.hasFacts ? 'success' : 'info'" effect="light" round>
+ {{ $t(runtimeSummary.hasFacts ? 'ai.drawer.hasFacts' : 'ai.drawer.noFacts') }}
+ </ElTag>
+ </div>
+
+ <ElAlert
+ v-if="runtime?.memorySummary"
+ type="info"
+ :closable="false"
+ :title="runtime.memorySummary"
+ />
+ <ElAlert
+ v-if="runtime?.memoryFacts"
+ type="success"
+ :closable="false"
+ :title="runtime.memoryFacts"
+ />
+ </div>
+ </ElCollapseTransition>
+ </div>
+
+ <ElAlert
+ v-if="drawerError"
+ type="warning"
+ :closable="false"
+ :title="drawerError"
+ class="rounded-2xl"
+ />
+
+ <div v-if="traceEvents.length" class="rounded-3xl bg-[var(--art-main-bg-color)] px-5 py-4 shadow-[0_12px_36px_rgba(15,23,42,0.05)] ring-1 ring-[var(--el-border-color-lighter)]">
+ <div class="mb-3 flex items-center justify-between gap-3">
+ <div class="text-sm font-semibold text-[var(--art-gray-900)]">{{ $t('ai.drawer.activityTrace') }}</div>
+ <ElTag effect="plain" round>{{ traceEvents.length }}</ElTag>
+ </div>
+ <div class="space-y-3">
<div
- :class="['flex max-w-[70%] flex-col', message.isMe ? 'items-end' : 'items-start']"
+ v-for="item in traceEvents"
+ :key="item.traceId"
+ class="rounded-2xl bg-g-100/55 px-4 py-3 ring-1 ring-[var(--el-border-color-extra-light)]"
+ >
+ <div class="flex flex-wrap items-center justify-between gap-3">
+ <div class="min-w-0">
+ <div class="flex items-center gap-2">
+ <ElTag size="small" :type="item.traceType === 'thinking' ? 'primary' : 'success'" effect="light">
+ {{ item.traceType === 'thinking' ? $t('ai.drawer.traceTypeThinking') : $t('ai.drawer.traceTypeTool') }}
+ </ElTag>
+ <span class="text-sm font-medium text-[var(--art-gray-900)]">
+ {{ item.toolName || item.title || $t('ai.drawer.unknownTool') }}
+ </span>
+ </div>
+ <div class="mt-1 text-xs text-[var(--art-gray-500)]">
+ {{ item.traceType === 'thinking' ? getThinkingStatusLabel(item.status) : getToolStatusLabel(item.status) }}
+ </div>
+ </div>
+
+ <ElButton
+ v-if="item.traceType !== 'thinking'"
+ text
+ size="small"
+ @click="toggleTraceEventExpanded(item.traceId)"
+ >
+ {{ $t(expandedTraceIds.includes(item.traceId) ? 'ai.drawer.collapseDetail' : 'ai.drawer.viewDetail') }}
+ </ElButton>
+ </div>
+
+ <div
+ v-if="item.traceType === 'thinking'"
+ class="mt-3 whitespace-pre-wrap break-words text-sm leading-6 text-[var(--art-gray-700)]"
+ >
+ {{ item.content || $t('ai.drawer.thinkingEmpty') }}
+ </div>
+
+ <div
+ v-else-if="expandedTraceIds.includes(item.traceId)"
+ class="mt-3 space-y-2 text-sm leading-6 text-[var(--art-gray-700)]"
+ >
+ <div v-if="item.title">{{ item.title }}</div>
+ <div v-if="item.inputSummary">{{ $t('ai.drawer.toolInput', { value: item.inputSummary }) }}</div>
+ <div v-if="item.outputSummary">{{ $t('ai.drawer.toolOutput', { value: item.outputSummary }) }}</div>
+ <div v-if="item.errorMessage" class="text-[var(--el-color-danger)]">
+ {{ $t('ai.drawer.toolError', { value: item.errorMessage }) }}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="flex min-h-0 flex-1 flex-col overflow-hidden rounded-3xl bg-[var(--art-main-bg-color)] shadow-[0_12px_36px_rgba(15,23,42,0.05)] ring-1 ring-[var(--el-border-color-lighter)]">
+ <div class="flex flex-wrap items-center justify-between gap-3 border-b border-[var(--el-border-color-extra-light)] px-5 py-4">
+ <div>
+ <div class="text-sm font-semibold text-[var(--art-gray-900)]">{{ currentSessionTitle }}</div>
+ <div class="mt-1 text-xs text-[var(--art-gray-500)]">
+ {{ usageSummaryText }}
+ </div>
+ </div>
+ <div class="flex flex-wrap gap-2">
+ <ElTag v-if="usage?.totalTokens != null" effect="plain" round>
+ {{ $t('ai.drawer.tokenMetric', {
+ prompt: usage?.promptTokens ?? 0,
+ completion: usage?.completionTokens ?? 0,
+ total: usage?.totalTokens ?? 0
+ }) }}
+ </ElTag>
+ </div>
+ </div>
+
+ <ElScrollbar class="min-h-0 flex-1 bg-g-100/35 px-5 py-5">
+ <div class="space-y-5">
+ <div v-if="!messages.length" class="rounded-3xl border border-dashed border-[var(--el-border-color)] bg-slate-50/80 px-4 py-8 text-center text-sm text-[var(--art-gray-500)]">
+ {{ $t('ai.drawer.emptyHint') }}
+ </div>
+
+ <div
+ v-for="(message, index) in messages"
+ :key="`${message.role}-${index}`"
+ class="flex items-end gap-3"
+ :class="message.role === 'user' ? 'justify-end' : 'justify-start'"
>
<div
- :class="[
- 'mb-1 flex gap-2 text-xs',
- message.isMe ? 'flex-row-reverse' : 'flex-row'
- ]"
+ v-if="message.role !== 'user'"
+ class="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-[var(--el-color-primary-light-9)] text-[var(--el-color-primary)]"
>
- <span class="font-medium">{{ message.sender }}</span>
- <span class="text-g-600">{{ message.time }}</span>
+ <ArtSvgIcon icon="ri:robot-2-line" class="text-base" />
</div>
- <div
- :class="[
- 'rounded-md px-3.5 py-2.5 text-sm leading-[1.4] text-g-900',
- message.isMe ? 'message-right bg-theme/15' : 'message-left bg-g-300/50'
- ]"
- >{{ message.content }}</div
- >
- </div>
- </div>
- </template>
- </div>
- <!-- 鑱婂ぉ杈撳叆鍖哄煙 -->
- <div class="px-4 pt-4">
- <ElInput
- v-model="messageText"
- type="textarea"
- :rows="3"
- placeholder="杈撳叆娑堟伅"
- resize="none"
- @keyup.enter.prevent="sendMessage"
- >
- <template #append>
- <div class="flex gap-2 py-2">
- <ElButton :icon="Paperclip" circle plain />
- <ElButton :icon="Picture" circle plain />
- <ElButton type="primary" @click="sendMessage" v-ripple>鍙戦��</ElButton>
+ <div class="max-w-[82%]">
+ <div
+ class="rounded-3xl px-4 py-3 text-sm leading-6 shadow-[0_10px_30px_rgba(15,23,42,0.04)]"
+ :class="
+ message.role === 'user'
+ ? 'rounded-br-xl bg-[var(--el-color-primary)] text-white'
+ : 'rounded-bl-xl bg-[var(--art-main-bg-color)] text-[var(--art-gray-900)] ring-1 ring-[var(--el-border-color-extra-light)]'
+ "
+ >
+ <div class="whitespace-pre-wrap break-words">
+ {{
+ message.role === 'assistant'
+ ? message.content || (streaming && index === messages.length - 1 ? $t('ai.drawer.thinking') : '')
+ : message.content || ''
+ }}
+ </div>
+ </div>
+ </div>
+
+ <div
+ v-if="message.role === 'user'"
+ class="flex size-9 shrink-0 items-center justify-center rounded-2xl bg-[var(--el-color-primary)] text-white"
+ >
+ <ArtSvgIcon icon="ri:user-3-line" class="text-base" />
+ </div>
</div>
- </template>
- </ElInput>
- <div class="mt-3 flex-cb">
- <div class="flex-c">
- <ArtSvgIcon icon="ri:image-line" class="mr-5 c-p text-g-600 text-lg" />
- <ArtSvgIcon icon="ri:emotion-happy-line" class="mr-5 c-p text-g-600 text-lg" />
+
+ <div ref="messagesBottomRef"></div>
</div>
- <ElButton type="primary" @click="sendMessage" v-ripple class="min-w-20">鍙戦��</ElButton>
+ </ElScrollbar>
+
+ <div class="border-t border-[var(--el-border-color-extra-light)] bg-[var(--art-main-bg-color)] px-5 py-4">
+ <div class="rounded-3xl bg-g-100/45 p-4 ring-1 ring-[var(--el-border-color-lighter)]">
+ <ElInput
+ v-model="input"
+ type="textarea"
+ :autosize="{ minRows: 3, maxRows: 6 }"
+ resize="none"
+ :placeholder="$t('ai.drawer.inputPlaceholder')"
+ @keydown="handleInputKeydown"
+ />
+
+ <div class="mt-3 flex flex-wrap items-center justify-between gap-3">
+ <div class="text-xs text-[var(--art-gray-500)]">
+ Enter 鍙戦�侊紝Shift + Enter 鎹㈣
+ </div>
+
+ <div class="flex flex-wrap items-center gap-2">
+ <ElButton text @click="input = ''">{{ $t('ai.drawer.clearInput') }}</ElButton>
+ <ElButton
+ v-if="streaming"
+ type="warning"
+ plain
+ @click="stopStream(true)"
+ >
+ {{ $t('ai.drawer.stop') }}
+ </ElButton>
+ <ElButton
+ v-else
+ type="primary"
+ @click="handleSend"
+ >
+ <ArtSvgIcon icon="ri:send-plane-2-line" class="mr-1 text-sm" />
+ {{ $t('ai.drawer.send') }}
+ </ElButton>
+ </div>
+ </div>
+ </div>
</div>
- </div>
+ </div>
+ </section>
</div>
- </ElDrawer>
- </div>
+ </div>
+
+ <ElDialog
+ v-model="renameDialog.open"
+ :title="$t('ai.drawer.renameDialogTitle')"
+ width="420px"
+ append-to-body
+ >
+ <ElInput
+ v-model="renameDialog.title"
+ :placeholder="$t('ai.drawer.sessionTitleField')"
+ />
+ <template #footer>
+ <ElButton @click="closeRenameDialog">{{ $t('common.cancel') }}</ElButton>
+ <ElButton
+ type="primary"
+ :disabled="streaming || !renameDialog.title.trim()"
+ @click="handleRenameSubmit"
+ >
+ {{ $t('common.confirm') }}
+ </ElButton>
+ </template>
+ </ElDialog>
+ </ElDrawer>
</template>
<script setup>
- import { Picture, Paperclip, Close } from '@element-plus/icons-vue'
-
+ import { ElMessage } from 'element-plus'
+ import { useRoute, useRouter } from 'vue-router'
+ import { useWindowSize } from '@vueuse/core'
+ import { useI18n } from 'vue-i18n'
import { mittBus } from '@/utils/sys'
- import meAvatar from '@/assets/images/avatar/avatar5.webp'
- import aiAvatar from '@/assets/images/avatar/avatar10.webp'
+ import {
+ DEFAULT_PROMPT_CODE,
+ fetchClearAiSessionMemory,
+ fetchGetAiRuntime,
+ fetchGetAiSessions,
+ fetchPinAiSession,
+ fetchRemoveAiSession,
+ fetchRenameAiSession,
+ fetchRetainAiSessionLatestRound,
+ fetchStreamAiChat
+ } from '@/api/ai-chat'
+
defineOptions({ name: 'ArtChatWindow' })
- const MOBILE_BREAKPOINT = 640
- const SCROLL_DELAY = 100
- const BOT_NAME = 'Art Bot'
- const USER_NAME = 'Ricky'
+
+ const MOBILE_BREAKPOINT = 768
+ const SESSION_SEARCH_DELAY = 250
+ const router = useRouter()
+ const route = useRoute()
const { width } = useWindowSize()
+ const { t } = useI18n()
+
const isMobile = computed(() => width.value < MOBILE_BREAKPOINT)
const isDrawerVisible = ref(false)
- const isOnline = ref(true)
- const messageText = ref('')
- const messageId = ref(10)
- const messageContainer = ref(null)
- const initializeMessages = () => [
- {
- id: 1,
- sender: BOT_NAME,
- content: '浣犲ソ锛佹垜鏄綘鐨凙I鍔╂墜锛屾湁浠�涔堟垜鍙互甯綘鐨勫悧锛�',
- time: '10:00',
- isMe: false,
- avatar: aiAvatar
- },
- {
- id: 2,
- sender: USER_NAME,
- content: '鎴戞兂浜嗚В涓�涓嬬郴缁熺殑浣跨敤鏂规硶銆�',
- time: '10:01',
- isMe: true,
- avatar: meAvatar
- },
- {
- id: 3,
- sender: BOT_NAME,
- content: '濂界殑锛屾垜鏉ヤ负鎮ㄤ粙缁嶇郴缁熺殑涓昏鍔熻兘銆傞鍏堬紝鎮ㄥ彲浠ラ�氳繃宸︿晶鑿滃崟璁块棶涓嶅悓鐨勫姛鑳芥ā鍧�...',
- time: '10:02',
- isMe: false,
- avatar: aiAvatar
- },
- {
- id: 4,
- sender: USER_NAME,
- content: '鍚捣鏉ュ緢涓嶉敊锛岃兘鍏蜂綋璁茶鏁版嵁鍒嗘瀽閮ㄥ垎鍚楋紵',
- time: '10:05',
- isMe: true,
- avatar: meAvatar
- },
- {
- id: 5,
- sender: BOT_NAME,
- content: '褰撶劧鍙互銆傛暟鎹垎鏋愭ā鍧楀彲浠ュ府鍔╂偍瀹炴椂鐩戞帶鍏抽敭鎸囨爣锛屽苟鐢熸垚璇︾粏鐨勬姤琛�...',
- time: '10:06',
- isMe: false,
- avatar: aiAvatar
- },
- {
- id: 6,
- sender: USER_NAME,
- content: '澶ソ浜嗭紝閭f垜濡備綍寮�濮嬩娇鐢ㄥ憿锛�',
- time: '10:08',
- isMe: true,
- avatar: meAvatar
- },
- {
- id: 7,
- sender: BOT_NAME,
- content: '鎮ㄥ彲浠ュ厛鍒涘缓涓�涓」鐩紝鐒跺悗鍦ㄩ」鐩腑娣诲姞鐩稿叧鐨勬暟鎹簮锛岀郴缁熶細鑷姩杩涜鍒嗘瀽銆�',
- time: '10:09',
- isMe: false,
- avatar: aiAvatar
- },
- {
- id: 8,
- sender: USER_NAME,
- content: '鏄庣櫧浜嗭紝璋㈣阿浣犵殑甯姪锛�',
- time: '10:10',
- isMe: true,
- avatar: meAvatar
- },
- {
- id: 9,
- sender: BOT_NAME,
- content: '涓嶅姘旓紝鏈変换浣曢棶棰橀殢鏃惰仈绯绘垜銆�',
- time: '10:11',
- isMe: false,
- avatar: aiAvatar
+ const runtime = ref(null)
+ const selectedAiParamId = ref(null)
+ const sessionId = ref(null)
+ const sessions = ref([])
+ const persistedMessages = ref([])
+ const messages = ref([])
+ const traceEvents = ref([])
+ const expandedTraceIds = ref([])
+ const input = ref('')
+ const usage = ref(null)
+ const drawerError = ref('')
+ const sessionKeyword = ref('')
+ const loadingRuntime = ref(false)
+ const streaming = ref(false)
+ const runtimePanelExpanded = ref(false)
+ const messagesBottomRef = ref(null)
+ const renameDialog = reactive({
+ open: false,
+ sessionId: null,
+ title: ''
+ })
+ const abortController = ref(null)
+ let sessionSearchTimer = null
+
+ const quickLinks = computed(() => [
+ { label: t('menu.aiParam'), path: '/system/ai-param' },
+ { label: t('menu.aiPrompt'), path: '/system/ai-prompt' },
+ { label: t('menu.aiMcpMount'), path: '/system/ai-mcp-mount' }
+ ])
+
+ const currentSessionRecord = computed(() =>
+ sessions.value.find((item) => item.sessionId === sessionId.value) || null
+ )
+
+ const currentSessionTitle = computed(() => {
+ return (
+ currentSessionRecord.value?.title ||
+ (sessionId.value ? t('ai.drawer.sessionTitle', { id: sessionId.value }) : t('ai.drawer.newSession'))
+ )
+ })
+
+ const runtimeSummary = computed(() => ({
+ requestId: runtime.value?.requestId || '--',
+ promptName: runtime.value?.promptName || '--',
+ model: runtime.value?.model || '--',
+ mountedMcpCount: runtime.value?.mountedMcpCount ?? 0,
+ recentMessageCount: runtime.value?.recentMessageCount ?? 0,
+ hasSummary: !!runtime.value?.memorySummary,
+ hasFacts: !!runtime.value?.memoryFacts
+ }))
+
+ const usageSummaryText = computed(() => {
+ if (loadingRuntime.value) {
+ return t('ai.drawer.loadingRuntime')
}
- ]
- const messages = ref(initializeMessages())
- const formatCurrentTime = () => {
- return /* @__PURE__ */ new Date().toLocaleTimeString([], {
- hour: '2-digit',
- minute: '2-digit'
- })
- }
- const scrollToBottom = () => {
- nextTick(() => {
- setTimeout(() => {
- if (messageContainer.value) {
- messageContainer.value.scrollTop = messageContainer.value.scrollHeight
+ if (usage.value?.elapsedMs != null) {
+ const firstTokenText =
+ usage.value?.firstTokenLatencyMs != null
+ ? ` / ${t('ai.drawer.firstTokenMetric', { value: usage.value.firstTokenLatencyMs })}`
+ : ''
+ return `${t('ai.drawer.elapsedMetric', { value: usage.value.elapsedMs })}${firstTokenText}`
+ }
+ return t('ai.drawer.modelSelectorHint')
+ })
+
+ const runtimeMetricCards = computed(() => [
+ {
+ label: t('ai.drawer.modelLabel'),
+ value: runtimeSummary.value.model,
+ icon: 'ri:cpu-line'
+ },
+ {
+ label: t('ai.drawer.promptLabel'),
+ value: runtimeSummary.value.promptName,
+ icon: 'ri:magic-line'
+ },
+ {
+ label: t('ai.drawer.mcpLabel'),
+ value: String(runtimeSummary.value.mountedMcpCount),
+ icon: 'ri:plug-2-line'
+ },
+ {
+ label: t('ai.drawer.historyLabel'),
+ value: String(persistedMessages.value.length),
+ icon: 'ri:chat-history-line'
+ }
+ ])
+
+ const selectableModelOptions = computed(() => {
+ if (Array.isArray(runtime.value?.modelOptions) && runtime.value.modelOptions.length) {
+ return runtime.value.modelOptions
+ }
+ if (runtime.value?.model) {
+ return [
+ {
+ aiParamId: runtime.value.aiParamId ?? 'CURRENT_MODEL',
+ name: runtime.value.model,
+ model: runtime.value.model,
+ active: true
}
- }, SCROLL_DELAY)
- })
- }
- const sendMessage = () => {
- const text = messageText.value.trim()
- if (!text) return
- const newMessage = {
- id: messageId.value++,
- sender: USER_NAME,
- content: text,
- time: formatCurrentTime(),
- isMe: true,
- avatar: meAvatar
+ ]
}
- messages.value.push(newMessage)
- messageText.value = ''
- scrollToBottom()
- }
- const openChat = () => {
- isDrawerVisible.value = true
- scrollToBottom()
- }
- const closeChat = () => {
- isDrawerVisible.value = false
- }
+ return []
+ })
+
+ watch(isDrawerVisible, async (visible) => {
+ if (visible) {
+ runtimePanelExpanded.value = false
+ await initializeDrawer()
+ scrollMessagesToBottom()
+ return
+ }
+ stopStream(false)
+ })
+
+ watch(
+ () => sessionKeyword.value,
+ () => {
+ if (!isDrawerVisible.value) {
+ return
+ }
+ if (sessionSearchTimer) {
+ window.clearTimeout(sessionSearchTimer)
+ }
+ sessionSearchTimer = window.setTimeout(() => {
+ void loadSessions()
+ }, SESSION_SEARCH_DELAY)
+ }
+ )
+
+ watch(
+ () => [messages.value.length, streaming.value, isDrawerVisible.value],
+ async ([, , visible]) => {
+ if (!visible) {
+ return
+ }
+ await nextTick()
+ scrollMessagesToBottom()
+ }
+ )
+
onMounted(() => {
- scrollToBottom()
mittBus.on('openChat', openChat)
})
+
onUnmounted(() => {
mittBus.off('openChat', openChat)
+ stopStream(false)
+ if (sessionSearchTimer) {
+ window.clearTimeout(sessionSearchTimer)
+ sessionSearchTimer = null
+ }
})
+
+ function getSessionActions(item) {
+ return [
+ {
+ key: item?.pinned ? 'unpin' : 'pin',
+ label: t(item?.pinned ? 'ai.drawer.unpinAction' : 'ai.drawer.pinAction'),
+ icon: item?.pinned ? 'ri:pushpin-line' : 'ri:pushpin-2-line',
+ disabled: streaming.value
+ },
+ {
+ key: 'rename',
+ label: t('ai.drawer.renameAction'),
+ icon: 'ri:edit-line',
+ disabled: streaming.value
+ },
+ {
+ key: 'delete',
+ label: t('ai.drawer.deleteAction'),
+ icon: 'ri:delete-bin-line',
+ color: 'var(--el-color-danger)',
+ disabled: streaming.value
+ }
+ ]
+ }
+
+ function openChat() {
+ isDrawerVisible.value = true
+ }
+
+ function closeChat() {
+ isDrawerVisible.value = false
+ }
+
+ async function initializeDrawer(targetSessionId = null) {
+ traceEvents.value = []
+ expandedTraceIds.value = []
+ usage.value = null
+ await Promise.all([loadRuntime(targetSessionId), loadSessions()])
+ }
+
+ async function loadRuntime(targetSessionId = null, targetAiParamId = selectedAiParamId.value) {
+ loadingRuntime.value = true
+ drawerError.value = ''
+ try {
+ const data = await fetchGetAiRuntime(DEFAULT_PROMPT_CODE, targetSessionId, targetAiParamId)
+ const historyMessages = normalizeMessageList(data?.persistedMessages)
+ runtime.value = data
+ selectedAiParamId.value = data?.aiParamId ?? targetAiParamId ?? null
+ sessionId.value = data?.sessionId || null
+ persistedMessages.value = historyMessages
+ messages.value = historyMessages
+ return data
+ } catch (error) {
+ drawerError.value = error?.message || t('ai.drawer.runtimeFailed')
+ return null
+ } finally {
+ loadingRuntime.value = false
+ }
+ }
+
+ async function loadSessions() {
+ try {
+ sessions.value = await fetchGetAiSessions(DEFAULT_PROMPT_CODE, sessionKeyword.value.trim())
+ } catch (error) {
+ drawerError.value = error?.message || t('ai.drawer.sessionListFailed')
+ sessions.value = []
+ }
+ }
+
+ async function startNewSession() {
+ if (streaming.value) {
+ return
+ }
+ sessionId.value = null
+ persistedMessages.value = []
+ messages.value = []
+ traceEvents.value = []
+ expandedTraceIds.value = []
+ usage.value = null
+ drawerError.value = ''
+ await loadRuntime(null, selectedAiParamId.value)
+ await loadSessions()
+ }
+
+ async function handleSwitchSession(targetSessionId) {
+ if (streaming.value || targetSessionId === sessionId.value) {
+ return
+ }
+ usage.value = null
+ traceEvents.value = []
+ expandedTraceIds.value = []
+ await loadRuntime(targetSessionId, selectedAiParamId.value)
+ }
+
+ async function handleModelChange(nextAiParamId) {
+ if (streaming.value) {
+ return
+ }
+ const previousAiParamId = selectedAiParamId.value
+ selectedAiParamId.value = nextAiParamId
+ const data = await loadRuntime(sessionId.value, nextAiParamId)
+ if (!data) {
+ selectedAiParamId.value = previousAiParamId
+ ElMessage.error(t('ai.drawer.modelSwitchFailed'))
+ }
+ }
+
+ async function handleDeleteSession(targetSessionId) {
+ if (streaming.value || !targetSessionId) {
+ return
+ }
+ try {
+ await fetchRemoveAiSession(targetSessionId)
+ ElMessage.success(t('ai.drawer.sessionDeleted'))
+ if (targetSessionId === sessionId.value) {
+ await startNewSession()
+ return
+ }
+ await loadSessions()
+ } catch (error) {
+ drawerError.value = error?.message || t('ai.drawer.deleteSessionFailed')
+ ElMessage.error(drawerError.value)
+ }
+ }
+
+ async function handlePinSession(targetSessionId, pinned) {
+ if (streaming.value || !targetSessionId) {
+ return
+ }
+ try {
+ await fetchPinAiSession(targetSessionId, pinned)
+ ElMessage.success(t(pinned ? 'ai.drawer.pinned' : 'ai.drawer.unpinned'))
+ await loadSessions()
+ } catch (error) {
+ drawerError.value = error?.message || t('ai.drawer.pinFailed')
+ ElMessage.error(drawerError.value)
+ }
+ }
+
+ function handleSessionAction(item, action) {
+ if (!item?.sessionId || !action?.key) {
+ return
+ }
+ if (action.key === 'pin') {
+ void handlePinSession(item.sessionId, true)
+ return
+ }
+ if (action.key === 'unpin') {
+ void handlePinSession(item.sessionId, false)
+ return
+ }
+ if (action.key === 'rename') {
+ openRenameDialog(item)
+ return
+ }
+ if (action.key === 'delete') {
+ void handleDeleteSession(item.sessionId)
+ }
+ }
+
+ function openRenameDialog(item) {
+ renameDialog.open = true
+ renameDialog.sessionId = item?.sessionId || null
+ renameDialog.title = item?.title || ''
+ }
+
+ function closeRenameDialog() {
+ renameDialog.open = false
+ renameDialog.sessionId = null
+ renameDialog.title = ''
+ }
+
+ async function handleRenameSubmit() {
+ if (streaming.value || !renameDialog.sessionId) {
+ return
+ }
+ try {
+ await fetchRenameAiSession(renameDialog.sessionId, renameDialog.title.trim())
+ ElMessage.success(t('ai.drawer.renamed'))
+ closeRenameDialog()
+ await loadSessions()
+ } catch (error) {
+ drawerError.value = error?.message || t('ai.drawer.renameFailed')
+ ElMessage.error(drawerError.value)
+ }
+ }
+
+ async function handleClearMemory() {
+ if (streaming.value || !sessionId.value) {
+ return
+ }
+ try {
+ await fetchClearAiSessionMemory(sessionId.value)
+ ElMessage.success(t('ai.drawer.memoryCleared'))
+ await Promise.all([loadRuntime(sessionId.value, selectedAiParamId.value), loadSessions()])
+ } catch (error) {
+ drawerError.value = error?.message || t('ai.drawer.clearMemoryFailed')
+ ElMessage.error(drawerError.value)
+ }
+ }
+
+ async function handleRetainLatestRound() {
+ if (streaming.value || !sessionId.value) {
+ return
+ }
+ try {
+ await fetchRetainAiSessionLatestRound(sessionId.value)
+ ElMessage.success(t('ai.drawer.retainLatestRoundSuccess'))
+ await Promise.all([loadRuntime(sessionId.value, selectedAiParamId.value), loadSessions()])
+ } catch (error) {
+ drawerError.value = error?.message || t('ai.drawer.retainLatestRoundFailed')
+ ElMessage.error(drawerError.value)
+ }
+ }
+
+ function stopStream(showTip = true) {
+ if (!abortController.value) {
+ return
+ }
+ abortController.value.abort()
+ abortController.value = null
+ streaming.value = false
+ if (showTip) {
+ ElMessage.success(t('ai.drawer.stopSuccess'))
+ }
+ }
+
+ function scrollMessagesToBottom() {
+ if (!messagesBottomRef.value) {
+ return
+ }
+ messagesBottomRef.value.scrollIntoView({ behavior: 'smooth', block: 'end' })
+ }
+
+ function appendAssistantDelta(delta) {
+ messages.value = (() => {
+ const next = [...messages.value]
+ const last = next[next.length - 1]
+ if (last && last.role === 'assistant') {
+ next[next.length - 1] = {
+ ...last,
+ content: `${last.content || ''}${delta}`
+ }
+ return next
+ }
+ next.push({ role: 'assistant', content: delta })
+ return next
+ })()
+ }
+
+ function ensureAssistantPlaceholder(seedMessages) {
+ const next = [...seedMessages]
+ const last = next[next.length - 1]
+ if (!last || last.role !== 'assistant') {
+ next.push({ role: 'assistant', content: '' })
+ }
+ return next
+ }
+
+ function appendTraceEvent(payload) {
+ if (!payload?.traceId) {
+ return
+ }
+ const index = traceEvents.value.findIndex((item) => item.traceId === payload.traceId)
+ if (index < 0) {
+ traceEvents.value = [...traceEvents.value, payload].sort(
+ (left, right) => (left?.sequence ?? 0) - (right?.sequence ?? 0)
+ )
+ return
+ }
+ const next = [...traceEvents.value]
+ next[index] = { ...next[index], ...payload }
+ traceEvents.value = next
+ }
+
+ function toggleTraceEventExpanded(traceId) {
+ if (!traceId) {
+ return
+ }
+ expandedTraceIds.value = expandedTraceIds.value.includes(traceId)
+ ? expandedTraceIds.value.filter((item) => item !== traceId)
+ : [...expandedTraceIds.value, traceId]
+ }
+
+ function getThinkingStatusLabel(status) {
+ if (status === 'COMPLETED') return t('ai.drawer.thinkingStatusCompleted')
+ if (status === 'FAILED') return t('ai.drawer.thinkingStatusFailed')
+ if (status === 'ABORTED') return t('ai.drawer.thinkingStatusAborted')
+ if (status === 'UPDATED') return t('ai.drawer.thinkingStatusUpdated')
+ return t('ai.drawer.thinkingStatusStarted')
+ }
+
+ function getToolStatusLabel(status) {
+ if (status === 'FAILED') return t('ai.drawer.toolStatusFailed')
+ if (status === 'COMPLETED') return t('ai.drawer.toolStatusCompleted')
+ return t('ai.drawer.toolStatusRunning')
+ }
+
+ async function handleSend() {
+ const content = input.value.trim()
+ if (!content || streaming.value) {
+ return
+ }
+
+ const nextMessages = [...messages.value, { role: 'user', content }]
+ input.value = ''
+ usage.value = null
+ drawerError.value = ''
+ traceEvents.value = []
+ expandedTraceIds.value = []
+ messages.value = ensureAssistantPlaceholder(nextMessages)
+ streaming.value = true
+
+ const controller = new AbortController()
+ abortController.value = controller
+
+ let completed = false
+ let completedSessionId = sessionId.value
+ let completedAiParamId = selectedAiParamId.value
+
+ try {
+ await fetchStreamAiChat(
+ {
+ sessionId: sessionId.value,
+ aiParamId: selectedAiParamId.value,
+ promptCode: runtime.value?.promptCode || DEFAULT_PROMPT_CODE,
+ messages: [{ role: 'user', content }],
+ metadata: {
+ path: route.fullPath
+ }
+ },
+ {
+ signal: controller.signal,
+ onEvent: (eventName, payload) => {
+ if (eventName === 'start') {
+ runtime.value = payload
+ selectedAiParamId.value = payload?.aiParamId ?? selectedAiParamId.value
+ if (payload?.sessionId) {
+ sessionId.value = payload.sessionId
+ completedSessionId = payload.sessionId
+ }
+ completedAiParamId = payload?.aiParamId ?? completedAiParamId
+ }
+ if (eventName === 'delta') {
+ appendAssistantDelta(payload?.content || '')
+ }
+ if (eventName === 'trace') {
+ appendTraceEvent(payload)
+ }
+ if (eventName === 'done') {
+ usage.value = payload
+ completed = true
+ if (payload?.sessionId) {
+ completedSessionId = payload.sessionId
+ }
+ }
+ if (eventName === 'error') {
+ const message = payload?.message || t('ai.drawer.chatFailed')
+ drawerError.value = payload?.requestId ? `${message} [${payload.requestId}]` : message
+ ElMessage.error(drawerError.value)
+ }
+ }
+ }
+ )
+ } catch (error) {
+ if (error?.name !== 'AbortError') {
+ drawerError.value = error?.message || t('ai.drawer.chatFailed')
+ ElMessage.error(drawerError.value)
+ }
+ } finally {
+ abortController.value = null
+ streaming.value = false
+ if (completed) {
+ await Promise.all([
+ loadRuntime(completedSessionId, completedAiParamId),
+ loadSessions()
+ ])
+ }
+ }
+ }
+
+ function handleInputKeydown(event) {
+ if (event.key === 'Enter' && !event.shiftKey) {
+ event.preventDefault()
+ void handleSend()
+ }
+ }
+
+ function navigateTo(path) {
+ closeChat()
+ void router.push(path)
+ }
+
+ function normalizeMessageList(list) {
+ if (!Array.isArray(list)) {
+ return []
+ }
+ return list
+ .filter((item) => item && ['user', 'assistant'].includes(item.role))
+ .map((item) => ({
+ role: item.role,
+ content: item.content || ''
+ }))
+ }
+
+ function formatModelOption(item) {
+ const label = item.name || item.model || '--'
+ const suffix =
+ item.model && item.name && item.name !== item.model ? ` / ${item.model}` : ''
+ const defaultMark = item.active ? ` ${t('ai.drawer.defaultModelSuffix')}` : ''
+ return `${label}${suffix}${defaultMark}`
+ }
</script>
+
+<style scoped>
+ .ai-chat-body {
+ display: flex;
+ }
+
+ .ai-chat-sidebar {
+ width: 320px;
+ }
+
+ :deep(.ai-chat-drawer .el-drawer__body) {
+ padding: 0;
+ }
+
+ :deep(.ai-chat-drawer .el-drawer) {
+ overflow: hidden;
+ }
+
+ :deep(.ai-chat-drawer .el-input__wrapper),
+ :deep(.ai-chat-drawer .el-textarea__inner),
+ :deep(.ai-chat-drawer .el-select__wrapper) {
+ box-shadow: none !important;
+ border: 1px solid var(--el-border-color-lighter);
+ border-radius: 16px;
+ }
+
+ :deep(.ai-chat-drawer .el-input__wrapper:hover),
+ :deep(.ai-chat-drawer .el-textarea__inner:hover),
+ :deep(.ai-chat-drawer .el-select__wrapper:hover) {
+ border-color: var(--el-color-primary-light-8);
+ }
+
+ :deep(.ai-chat-drawer .el-input__wrapper.is-focus),
+ :deep(.ai-chat-drawer .el-select__wrapper.is-focused) {
+ border-color: var(--el-color-primary-light-7);
+ box-shadow: 0 0 0 3px var(--el-color-primary-light-9) !important;
+ }
+
+ :deep(.ai-chat-drawer .el-textarea__inner),
+ :deep(.ai-chat-drawer .el-select__wrapper),
+ :deep(.ai-chat-drawer .el-input__wrapper) {
+ background: var(--art-main-bg-color);
+ }
+
+ @media screen and (max-width: 768px) {
+ .ai-chat-body {
+ flex-direction: column;
+ }
+
+ .ai-chat-sidebar {
+ width: 100%;
+ max-height: 280px;
+ border-right: 0;
+ border-bottom: 1px solid var(--art-border-color);
+ }
+ }
+</style>
diff --git a/rsf-design/src/locales/langs/en.json b/rsf-design/src/locales/langs/en.json
index 6a5f6f8..8572b24 100644
--- a/rsf-design/src/locales/langs/en.json
+++ b/rsf-design/src/locales/langs/en.json
@@ -389,6 +389,91 @@
"taskPathTemplateMerge": "TaskPathTemplateMerge",
"missionFlowStepInstance": "Mission Flow Steps"
},
+ "ai": {
+ "drawer": {
+ "title": "WMS Assistant",
+ "runtimeFailed": "Failed to load AI runtime",
+ "sessionListFailed": "Failed to load AI sessions",
+ "sessionDeleted": "Session deleted",
+ "deleteSessionFailed": "Failed to delete AI session",
+ "pinned": "Session pinned",
+ "unpinned": "Session unpinned",
+ "pinFailed": "Failed to update session pin state",
+ "renamed": "Session renamed",
+ "renameFailed": "Failed to rename session",
+ "memoryCleared": "Session memory cleared",
+ "clearMemoryFailed": "Failed to clear session memory",
+ "retainLatestRoundSuccess": "Only the latest round was kept",
+ "retainLatestRoundFailed": "Failed to keep only the latest round",
+ "stopSuccess": "Current output stopped",
+ "chatFailed": "AI chat failed",
+ "newSession": "New Session",
+ "sessionList": "Sessions",
+ "searchPlaceholder": "Search session titles",
+ "noSessions": "No history sessions",
+ "sessionTitle": "Session %{id}",
+ "pinAction": "Pin session",
+ "unpinAction": "Unpin session",
+ "renameAction": "Rename session",
+ "deleteAction": "Delete session",
+ "activityTrace": "Thinking & Tool Trace",
+ "thinkingEmpty": "Organizing the current stage information...",
+ "thinkingStatusStarted": "Started",
+ "thinkingStatusUpdated": "In Progress",
+ "thinkingStatusCompleted": "Completed",
+ "thinkingStatusFailed": "Failed",
+ "thinkingStatusAborted": "Aborted",
+ "unknownTool": "Unknown tool",
+ "traceTypeThinking": "Thinking",
+ "traceTypeTool": "Tool",
+ "toolStatusFailed": "Failed",
+ "toolStatusCompleted": "Completed",
+ "toolStatusRunning": "Running",
+ "collapseDetail": "Hide Details",
+ "viewDetail": "View Details",
+ "toolInput": "Input: %{value}",
+ "toolOutput": "Output summary: %{value}",
+ "toolError": "Error: %{value}",
+ "hasSummary": "Summary",
+ "noSummary": "No Summary",
+ "hasFacts": "Facts",
+ "noFacts": "No Facts",
+ "retainLatestRound": "Keep Latest Round",
+ "clearMemory": "Clear Memory",
+ "runtimeOverview": "Runtime Overview",
+ "runtimeExpand": "Show Overview",
+ "runtimeCollapse": "Hide Overview",
+ "loadingRuntime": "Loading AI runtime info...",
+ "emptyHint": "AI responses stream back through SSE here. You can also maintain parameters, prompts, and MCP mounts from the quick links above.",
+ "userRole": "You",
+ "assistantRole": "AI",
+ "thinking": "Thinking...",
+ "inputPlaceholder": "Type your question. Press Enter to send, Shift + Enter for a new line",
+ "clearInput": "Clear Input",
+ "stop": "Stop",
+ "send": "Send",
+ "renameDialogTitle": "Rename Session",
+ "sessionTitleField": "Session Title",
+ "requestMetric": "Req: %{value}",
+ "sessionMetric": "Session: %{id}",
+ "promptMetric": "Prompt: %{value}",
+ "modelMetric": "Model: %{value}",
+ "promptLabel": "Prompt",
+ "modelLabel": "Model",
+ "modelSelectorLabel": "Chat Model",
+ "modelSelectorHint": "Switching only affects subsequent replies in this session and does not change the global default model.",
+ "modelSwitchFailed": "Failed to switch the chat model",
+ "defaultModelSuffix": "(Default)",
+ "mcpMetric": "MCP: %{value}",
+ "historyMetric": "History: %{value}",
+ "mcpLabel": "MCP",
+ "historyLabel": "History",
+ "recentMetric": "Recent: %{value}",
+ "elapsedMetric": "Elapsed: %{value} ms",
+ "firstTokenMetric": "First token: %{value} ms",
+ "tokenMetric": "Tokens: prompt %{prompt} / completion %{completion} / total %{total}"
+ }
+ },
"table": {
"form": {
"reset": "Reset",
diff --git a/rsf-design/src/locales/langs/zh.json b/rsf-design/src/locales/langs/zh.json
index ba38b1d..716e9cd 100644
--- a/rsf-design/src/locales/langs/zh.json
+++ b/rsf-design/src/locales/langs/zh.json
@@ -391,6 +391,91 @@
"taskPathTemplateMerge": "浠诲姟璺緞妯℃澘鍚堝苟",
"missionFlowStepInstance": "浠诲姟娴佺▼姝ラ"
},
+ "ai": {
+ "drawer": {
+ "title": "WMS 鍔╂墜",
+ "runtimeFailed": "鑾峰彇 AI 杩愯鏃跺け璐�",
+ "sessionListFailed": "鑾峰彇 AI 浼氳瘽鍒楄〃澶辫触",
+ "sessionDeleted": "浼氳瘽宸插垹闄�",
+ "deleteSessionFailed": "鍒犻櫎 AI 浼氳瘽澶辫触",
+ "pinned": "浼氳瘽宸茬疆椤�",
+ "unpinned": "浼氳瘽宸插彇娑堢疆椤�",
+ "pinFailed": "鏇存柊浼氳瘽缃《鐘舵�佸け璐�",
+ "renamed": "浼氳瘽宸查噸鍛藉悕",
+ "renameFailed": "閲嶅懡鍚嶄細璇濆け璐�",
+ "memoryCleared": "浼氳瘽璁板繂宸叉竻绌�",
+ "clearMemoryFailed": "娓呯┖浼氳瘽璁板繂澶辫触",
+ "retainLatestRoundSuccess": "宸蹭粎淇濈暀褰撳墠杞蹇�",
+ "retainLatestRoundFailed": "淇濈暀褰撳墠杞蹇嗗け璐�",
+ "stopSuccess": "宸插仠姝㈠綋鍓嶅璇濊緭鍑�",
+ "chatFailed": "AI 瀵硅瘽澶辫触",
+ "newSession": "鏂板缓浼氳瘽",
+ "sessionList": "浼氳瘽鍒楄〃",
+ "searchPlaceholder": "鎼滅储浼氳瘽鏍囬",
+ "noSessions": "鏆傛棤鍘嗗彶浼氳瘽",
+ "sessionTitle": "浼氳瘽 %{id}",
+ "pinAction": "缃《浼氳瘽",
+ "unpinAction": "鍙栨秷缃《",
+ "renameAction": "閲嶅懡鍚嶄細璇�",
+ "deleteAction": "鍒犻櫎浼氳瘽",
+ "activityTrace": "鎬濈淮閾句笌宸ュ叿杞ㄨ抗",
+ "thinkingEmpty": "姝e湪鏁寸悊褰撳墠闃舵淇℃伅...",
+ "thinkingStatusStarted": "宸插紑濮�",
+ "thinkingStatusUpdated": "杩涜涓�",
+ "thinkingStatusCompleted": "宸插畬鎴�",
+ "thinkingStatusFailed": "澶辫触",
+ "thinkingStatusAborted": "宸蹭腑姝�",
+ "unknownTool": "鏈煡宸ュ叿",
+ "traceTypeThinking": "鎬濈淮閾�",
+ "traceTypeTool": "宸ュ叿",
+ "toolStatusFailed": "澶辫触",
+ "toolStatusCompleted": "瀹屾垚",
+ "toolStatusRunning": "鎵ц涓�",
+ "collapseDetail": "鏀惰捣璇︽儏",
+ "viewDetail": "鏌ョ湅璇︽儏",
+ "toolInput": "鍏ュ弬: %{value}",
+ "toolOutput": "缁撴灉鎽樿: %{value}",
+ "toolError": "閿欒: %{value}",
+ "hasSummary": "鏈夋憳瑕�",
+ "noSummary": "鏃犳憳瑕�",
+ "hasFacts": "鏈変簨瀹�",
+ "noFacts": "鏃犱簨瀹�",
+ "retainLatestRound": "浠呬繚鐣欏綋鍓嶈疆",
+ "clearMemory": "娓呯┖璁板繂",
+ "runtimeOverview": "杩愯姒傝",
+ "runtimeExpand": "灞曞紑姒傝",
+ "runtimeCollapse": "鏀惰捣姒傝",
+ "loadingRuntime": "姝e湪鍔犺浇 AI 杩愯鏃朵俊鎭�...",
+ "emptyHint": "杩欓噷浼氶�氳繃 SSE 娴佸紡杩斿洖 AI 鍥炲銆備綘涔熷彲浠ュ厛鍘讳笂闈㈢殑蹇嵎鍏ュ彛缁存姢鍙傛暟銆丳rompt 鍜� MCP 鎸傝浇銆�",
+ "userRole": "浣�",
+ "assistantRole": "AI",
+ "thinking": "鎬濊�冧腑...",
+ "inputPlaceholder": "杈撳叆浣犵殑闂锛屾寜 Enter 鍙戦�侊紝Shift + Enter 鎹㈣",
+ "clearInput": "娓呯┖杈撳叆",
+ "stop": "鍋滄",
+ "send": "鍙戦��",
+ "renameDialogTitle": "閲嶅懡鍚嶄細璇�",
+ "sessionTitleField": "浼氳瘽鏍囬",
+ "requestMetric": "Req: %{value}",
+ "sessionMetric": "Session: %{id}",
+ "promptMetric": "Prompt: %{value}",
+ "modelMetric": "Model: %{value}",
+ "promptLabel": "Prompt",
+ "modelLabel": "Model",
+ "modelSelectorLabel": "瀵硅瘽妯″瀷",
+ "modelSelectorHint": "鍒囨崲鍚庝粎褰卞搷褰撳墠浼氳瘽鍚庣画鍥炲锛屼笉浼氭敼鍔ㄥ叏灞�榛樿妯″瀷銆�",
+ "modelSwitchFailed": "鍒囨崲瀵硅瘽妯″瀷澶辫触",
+ "defaultModelSuffix": "(榛樿)",
+ "mcpMetric": "MCP: %{value}",
+ "historyMetric": "History: %{value}",
+ "mcpLabel": "MCP",
+ "historyLabel": "History",
+ "recentMetric": "Recent: %{value}",
+ "elapsedMetric": "鑰楁椂: %{value} ms",
+ "firstTokenMetric": "棣栧寘: %{value} ms",
+ "tokenMetric": "Tokens: prompt %{prompt} / completion %{completion} / total %{total}"
+ }
+ },
"table": {
"form": {
"reset": "閲嶇疆",
diff --git a/rsf-design/src/plugins/iconify.collections.js b/rsf-design/src/plugins/iconify.collections.js
index a56d4b7..d729dbb 100644
--- a/rsf-design/src/plugins/iconify.collections.js
+++ b/rsf-design/src/plugins/iconify.collections.js
@@ -112,6 +112,9 @@
'book-2-line': {
body: '<path fill="currentColor" d="M21 18H6a1 1 0 1 0 0 2h15v2H6a3 3 0 0 1-3-3V4a2 2 0 0 1 2-2h16zM5 16.05q.243-.05.5-.05H19V4H5zM16 9H8V7h8z"/>'
},
+ 'chat-1-line': {
+ body: '<path fill="currentColor" d="M10 3h4a8 8 0 1 1 0 16v3.5c-5-2-12-5-12-11.5a8 8 0 0 1 8-8m2 14h2a6 6 0 0 0 0-12h-4a6 6 0 0 0-6 6c0 3.61 2.462 5.966 8 8.48z"/>'
+ },
'check-fill': {
body: '<path fill="currentColor" d="m10 15.17l9.192-9.191l1.414 1.414L10 17.999l-6.364-6.364l1.414-1.414z"/>'
},
@@ -163,9 +166,6 @@
'edit-2-line': {
body: '<path fill="currentColor" d="M5 18.89h1.414l9.314-9.314l-1.414-1.414L5 17.476zm16 2H3v-4.243L16.435 3.212a1 1 0 0 1 1.414 0l2.829 2.829a1 1 0 0 1 0 1.414L9.243 18.89H21zM15.728 6.748l1.414 1.414l1.414-1.414l-1.414-1.414z"/>'
},
- 'emotion-happy-line': {
- body: '<path fill="currentColor" d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10m0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16m-5-7h2a3 3 0 1 0 6 0h2a5 5 0 0 1-10 0m1-2a1.5 1.5 0 1 1 0-3a1.5 1.5 0 0 1 0 3m8 0a1.5 1.5 0 1 1 0-3a1.5 1.5 0 0 1 0 3"/>'
- },
'error-warning-line': {
body: '<path fill="currentColor" d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10m0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16m-1-5h2v2h-2zm0-8h2v6h-2z"/>'
},
@@ -210,9 +210,6 @@
},
'home-smile-2-line': {
body: '<path fill="currentColor" d="M19 19V9.799l-7-5.522l-7 5.522V19zm2 1a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9.314a1 1 0 0 1 .38-.785l8-6.311a1 1 0 0 1 1.24 0l8 6.31a1 1 0 0 1 .38.786zM7 12h2a3 3 0 1 0 6 0h2a5 5 0 0 1-10 0"/>'
- },
- 'image-line': {
- body: '<path fill="currentColor" d="M2.992 21A.993.993 0 0 1 2 20.007V3.993A1 1 0 0 1 2.992 3h18.016c.548 0 .992.445.992.993v16.014a1 1 0 0 1-.992.993zM20 15V5H4v14L14 9zm0 2.828l-6-6L6.828 19H20zM8 11a2 2 0 1 1 0-4a2 2 0 0 1 0 4"/>'
},
'key-2-line': {
body: '<path fill="currentColor" d="m10.758 11.828l7.849-7.849l1.414 1.414l-1.414 1.415l2.474 2.474l-1.414 1.415l-2.475-2.475l-1.414 1.414l2.121 2.121l-1.414 1.415l-2.121-2.122l-2.192 2.192a5.002 5.002 0 0 1-7.708 6.293a5 5 0 0 1 6.294-7.707m-.637 6.293A3 3 0 1 0 5.88 13.88a3 3 0 0 0 4.242 4.242"/>'
@@ -301,6 +298,9 @@
'pulse-line': {
body: '<path fill="currentColor" d="m9 7.539l6 14L18.66 13H23v-2h-5.66L15 16.461l-6-14L5.34 11H1v2h5.66z"/>'
},
+ 'pushpin-2-fill': {
+ body: '<path fill="currentColor" d="M18 3v2h-1v6l2 3v2h-6v7h-2v-7H5v-2l2-3V5H6V3z"/>'
+ },
'pushpin-2-line': {
body: '<path fill="currentColor" d="M18 3v2h-1v6l2 3v2h-6v7h-2v-7H5v-2l2-3V5H6V3zM9 5v6.606L7.404 14h9.192L15 11.606V5z"/>'
},
diff --git a/rsf-design/src/views/abnormal/index.vue b/rsf-design/src/views/abnormal/index.vue
index c6bdf38..46e14a1 100644
--- a/rsf-design/src/views/abnormal/index.vue
+++ b/rsf-design/src/views/abnormal/index.vue
@@ -9,13 +9,20 @@
>
<div class="min-w-0 pr-6">
<p class="text-sm font-medium text-g-700">{{ item.title }}</p>
- <ArtCountTo class="mt-3 block text-[2.3rem] font-semibold leading-none text-g-900" :target="item.total" :duration="1200" />
+ <ArtCountTo
+ class="mt-3 block text-[2.3rem] font-semibold leading-none text-g-900"
+ :target="item.total"
+ :duration="1200"
+ />
<div class="mt-4 flex items-center gap-2 text-sm">
<span class="text-g-500">{{ item.badgeText }}</span>
<span class="text-g-600">{{ item.subtitle }}</span>
</div>
</div>
- <div class="flex size-13 shrink-0 items-center justify-center rounded-2xl" :class="item.iconBoxClass">
+ <div
+ class="flex size-13 shrink-0 items-center justify-center rounded-2xl"
+ :class="item.iconBoxClass"
+ >
<ArtSvgIcon :icon="item.icon" class="text-2xl" :class="item.iconClass" />
</div>
</div>
@@ -39,11 +46,15 @@
@click="navigateTo(item.route)"
>
<div class="flex min-w-0 items-center gap-3">
- <div class="flex size-11 shrink-0 items-center justify-center rounded-2xl bg-[var(--el-color-primary-light-9)]">
+ <div
+ class="flex size-11 shrink-0 items-center justify-center rounded-2xl bg-[var(--el-color-primary-light-9)]"
+ >
<ArtSvgIcon :icon="item.icon" class="text-xl text-[var(--el-color-primary)]" />
</div>
<div class="min-w-0">
- <p class="truncate text-sm font-medium text-[var(--art-gray-900)]">{{ item.title }}</p>
+ <p class="truncate text-sm font-medium text-[var(--art-gray-900)]">{{
+ item.title
+ }}</p>
<p class="mt-1 truncate text-xs text-g-500">{{ item.description }}</p>
</div>
</div>
@@ -76,7 +87,9 @@
</div>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
- <p class="truncate text-base font-medium text-[var(--art-gray-900)]">{{ item.title }}</p>
+ <p class="truncate text-base font-medium text-[var(--art-gray-900)]">{{
+ item.title
+ }}</p>
<ElTag size="small" effect="light" type="danger">{{ item.source }}</ElTag>
</div>
<p class="mt-2 text-sm text-g-600">{{ item.summary }}</p>
@@ -129,12 +142,19 @@
async function loadOverview() {
sectionLoading.summary = true
- const [checkDiffResponse, qlyInspectResponse, freezeResponse, locReviseResponse] = await Promise.all([
- withRequestGuard(fetchCheckDiffPage({ current: 1, pageSize: 5 }), { records: [], total: 0 }),
- withRequestGuard(fetchQlyInspectPage({ current: 1, pageSize: 5 }), { records: [], total: 0 }),
- withRequestGuard(fetchFreezePage({ current: 1, pageSize: 5 }), { records: [], total: 0 }),
- withRequestGuard(fetchLocRevisePage({ current: 1, pageSize: 5 }), { records: [], total: 0 })
- ])
+ const [checkDiffResponse, qlyInspectResponse, freezeResponse, locReviseResponse] =
+ await Promise.all([
+ withRequestGuard(fetchCheckDiffPage({ current: 1, pageSize: 5 }), {
+ records: [],
+ total: 0
+ }),
+ withRequestGuard(fetchQlyInspectPage({ current: 1, pageSize: 5 }), {
+ records: [],
+ total: 0
+ }),
+ withRequestGuard(fetchFreezePage({ current: 1, pageSize: 5 }), { records: [], total: 0 }),
+ withRequestGuard(fetchLocRevisePage({ current: 1, pageSize: 5 }), { records: [], total: 0 })
+ ])
overviewState.value = {
checkDiff: normalizeSummaryResponse(checkDiffResponse),
diff --git a/rsf-design/src/views/manager/loc-preview/modules/loc-preview-detail-drawer.vue b/rsf-design/src/views/manager/loc-preview/modules/loc-preview-detail-drawer.vue
index b9c3a65..b53a816 100644
--- a/rsf-design/src/views/manager/loc-preview/modules/loc-preview-detail-drawer.vue
+++ b/rsf-design/src/views/manager/loc-preview/modules/loc-preview-detail-drawer.vue
@@ -10,7 +10,9 @@
<ElDescriptionsItem label="搴撲綅缂栫爜">{{ detail.locCode || '--' }}</ElDescriptionsItem>
<ElDescriptionsItem label="浠撳簱">{{ detail.warehouseLabel || '--' }}</ElDescriptionsItem>
<ElDescriptionsItem label="搴撳尯">{{ detail.areaLabel || '--' }}</ElDescriptionsItem>
- <ElDescriptionsItem label="浣跨敤鐘舵��">{{ detail.useStatusLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浣跨敤鐘舵��">{{
+ detail.useStatusLabel || '--'
+ }}</ElDescriptionsItem>
<ElDescriptionsItem label="搴撲綅绫诲瀷">{{ detail.typeLabel || '--' }}</ElDescriptionsItem>
<ElDescriptionsItem label="鏉$爜">{{ detail.barcode || '--' }}</ElDescriptionsItem>
<ElDescriptionsItem label="鎺�">{{ detail.row ?? '--' }}</ElDescriptionsItem>
diff --git a/rsf-design/src/views/system/ai-mcp-mount/index.vue b/rsf-design/src/views/system/ai-mcp-mount/index.vue
index 9170481..66548a5 100644
--- a/rsf-design/src/views/system/ai-mcp-mount/index.vue
+++ b/rsf-design/src/views/system/ai-mcp-mount/index.vue
@@ -12,7 +12,9 @@
<div class="mb-5 flex flex-wrap items-center justify-between gap-4">
<div>
<h3 class="text-lg font-semibold text-[var(--art-gray-900)]">MCP 鎸傝浇</h3>
- <p class="mt-1 text-sm text-[var(--art-gray-500)]">鎸変紶杈撶被鍨嬬鐞� MCP 鎸傝浇銆佽繛閫氭�у拰宸ュ叿棰勮銆�</p>
+ <p class="mt-1 text-sm text-[var(--art-gray-500)]"
+ >鎸変紶杈撶被鍨嬬鐞� MCP 鎸傝浇銆佽繛閫氭�у拰宸ュ叿棰勮銆�</p
+ >
</div>
<ElSpace wrap>
@@ -39,12 +41,18 @@
<div class="flex items-start justify-between gap-4">
<div class="min-w-0">
<div class="flex items-center gap-3">
- <div class="flex size-11 shrink-0 items-center justify-center rounded-2xl bg-emerald-50 text-emerald-600">
+ <div
+ class="flex size-11 shrink-0 items-center justify-center rounded-2xl bg-emerald-50 text-emerald-600"
+ >
<ArtSvgIcon icon="ri:plug-2-line" class="text-xl" />
</div>
<div class="min-w-0">
- <h4 class="truncate text-base font-semibold text-[var(--art-gray-900)]">{{ item.name || '--' }}</h4>
- <p class="mt-1 truncate text-sm text-[var(--art-gray-500)]">{{ item.transportText }}</p>
+ <h4 class="truncate text-base font-semibold text-[var(--art-gray-900)]">{{
+ item.name || '--'
+ }}</h4>
+ <p class="mt-1 truncate text-sm text-[var(--art-gray-500)]">{{
+ item.transportText
+ }}</p>
</div>
</div>
</div>
@@ -56,20 +64,30 @@
</div>
<div class="mt-4 grid gap-3 text-sm sm:grid-cols-2">
- <div class="rounded-2xl bg-[var(--art-main-bg-color)]/70 p-3 ring-1 ring-inset ring-[var(--art-border-color)]">
+ <div
+ class="rounded-2xl bg-[var(--art-main-bg-color)]/70 p-3 ring-1 ring-inset ring-[var(--art-border-color)]"
+ >
<p class="text-xs text-[var(--art-gray-500)]">鐩爣鍦板潃</p>
- <p class="mt-2 break-all text-[var(--art-gray-900)]">{{ item.targetLabel || '--' }}</p>
+ <p class="mt-2 break-all text-[var(--art-gray-900)]">{{
+ item.targetLabel || '--'
+ }}</p>
</div>
- <div class="rounded-2xl bg-[var(--art-main-bg-color)]/70 p-3 ring-1 ring-inset ring-[var(--art-border-color)]">
+ <div
+ class="rounded-2xl bg-[var(--art-main-bg-color)]/70 p-3 ring-1 ring-inset ring-[var(--art-border-color)]"
+ >
<p class="text-xs text-[var(--art-gray-500)]">鏈�杩戞祴璇�</p>
- <p class="mt-2 text-[var(--art-gray-900)]">{{ item['lastTestTime$'] || '鏈祴璇�' }}</p>
+ <p class="mt-2 text-[var(--art-gray-900)]">{{
+ item['lastTestTime$'] || '鏈祴璇�'
+ }}</p>
</div>
</div>
<div class="mt-4 grid gap-3 text-sm sm:grid-cols-3">
<div class="rounded-2xl bg-slate-50 px-3 py-2">
<p class="text-xs text-[var(--art-gray-500)]">瓒呮椂</p>
- <p class="mt-1 font-medium text-[var(--art-gray-900)]">{{ item.requestTimeoutMs ?? '--' }} ms</p>
+ <p class="mt-1 font-medium text-[var(--art-gray-900)]"
+ >{{ item.requestTimeoutMs ?? '--' }} ms</p
+ >
</div>
<div class="rounded-2xl bg-slate-50 px-3 py-2">
<p class="text-xs text-[var(--art-gray-500)]">鎺掑簭</p>
@@ -77,17 +95,25 @@
</div>
<div class="rounded-2xl bg-slate-50 px-3 py-2">
<p class="text-xs text-[var(--art-gray-500)]">鍒濆鍖栬�楁椂</p>
- <p class="mt-1 font-medium text-[var(--art-gray-900)]">{{ item.lastInitElapsedMs ?? '--' }}</p>
+ <p class="mt-1 font-medium text-[var(--art-gray-900)]">{{
+ item.lastInitElapsedMs ?? '--'
+ }}</p>
</div>
</div>
<div class="mt-4 rounded-2xl bg-amber-50/80 px-4 py-3">
<p class="text-xs text-[var(--art-gray-500)]">澶囨敞</p>
- <p class="mt-2 line-clamp-3 text-sm leading-6 text-[var(--art-gray-900)]">{{ item.memo || '--' }}</p>
+ <p class="mt-2 line-clamp-3 text-sm leading-6 text-[var(--art-gray-900)]">{{
+ item.memo || '--'
+ }}</p>
</div>
- <div class="mt-5 flex flex-wrap items-center justify-between gap-3 border-t border-[var(--art-border-color)] pt-4">
- <div class="text-xs text-[var(--art-gray-500)]">{{ item['updateTime$'] || '--' }}</div>
+ <div
+ class="mt-5 flex flex-wrap items-center justify-between gap-3 border-t border-[var(--art-border-color)] pt-4"
+ >
+ <div class="text-xs text-[var(--art-gray-500)]">{{
+ item['updateTime$'] || '--'
+ }}</div>
<ElSpace wrap>
<ElButton text @click="openDetailDialog(item)">璇︽儏</ElButton>
@@ -101,7 +127,9 @@
杩為�氭�ф祴璇�
</ElButton>
<ElButton v-auth="'list'" text @click="openToolsDrawer(item)">宸ュ叿棰勮</ElButton>
- <ElButton v-auth="'remove'" text type="danger" @click="handleDelete(item)">鍒犻櫎</ElButton>
+ <ElButton v-auth="'remove'" text type="danger" @click="handleDelete(item)"
+ >鍒犻櫎</ElButton
+ >
</ElSpace>
</div>
</article>
@@ -253,7 +281,9 @@
async function openEditDialog(record) {
try {
- currentMcpMountData.value = buildAiMcpMountDialogModel(await fetchGetAiMcpMountDetail(record.id))
+ currentMcpMountData.value = buildAiMcpMountDialogModel(
+ await fetchGetAiMcpMountDetail(record.id)
+ )
dialogMode.value = 'edit'
dialogVisible.value = true
} catch {
@@ -263,7 +293,9 @@
async function openDetailDialog(record) {
try {
- currentMcpMountData.value = buildAiMcpMountDialogModel(await fetchGetAiMcpMountDetail(record.id))
+ currentMcpMountData.value = buildAiMcpMountDialogModel(
+ await fetchGetAiMcpMountDetail(record.id)
+ )
dialogMode.value = 'show'
dialogVisible.value = true
} catch {
diff --git a/rsf-design/src/views/system/role/index.vue b/rsf-design/src/views/system/role/index.vue
index 49d2e34..2833f51 100644
--- a/rsf-design/src/views/system/role/index.vue
+++ b/rsf-design/src/views/system/role/index.vue
@@ -24,9 +24,9 @@
:disabled="selectedRows.length === 0"
@click="handleBatchDelete"
v-ripple
- >
- 鎵归噺鍒犻櫎
- </ElButton>
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
<span v-auth="'query'" class="inline-flex">
<ListExportPrint
:preview-visible="previewVisible"
@@ -218,13 +218,17 @@
}
const resolvePrintRecords = async (payload) => {
- const response = Array.isArray(payload?.ids) && payload.ids.length > 0
- ? await fetchGetRoleMany(payload.ids)
- : await fetchRolePrintPage({
- ...reportQueryParams.value,
- current: 1,
- pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
- })
+ const response =
+ Array.isArray(payload?.ids) && payload.ids.length > 0
+ ? await fetchGetRoleMany(payload.ids)
+ : await fetchRolePrintPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize:
+ Number(pagination.total) > 0
+ ? Number(pagination.total)
+ : Number(payload?.pageSize) || 20
+ })
return defaultResponseAdapter(response).records
}
diff --git a/rsf-design/src/views/system/role/rolePage.helpers.js b/rsf-design/src/views/system/role/rolePage.helpers.js
index d01d937..0b2696f 100644
--- a/rsf-design/src/views/system/role/rolePage.helpers.js
+++ b/rsf-design/src/views/system/role/rolePage.helpers.js
@@ -28,7 +28,9 @@
}
return Object.fromEntries(
- Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ Object.entries(searchParams).filter(
+ ([, value]) => value !== '' && value !== void 0 && value !== null
+ )
)
}
@@ -86,7 +88,8 @@
return null
}
- const source = ROLE_REPORT_SOURCE_ALIAS[column.source ?? column.prop] ?? column.source ?? column.prop
+ const source =
+ ROLE_REPORT_SOURCE_ALIAS[column.source ?? column.prop] ?? column.source ?? column.prop
if (!source || !allowedColumns.has(source) || seenSources.has(source)) {
return null
}
@@ -224,9 +227,7 @@
return []
}
- return treeData
- .map((node) => normalizeRoleScopeNode(scopeType, node))
- .filter(Boolean)
+ return treeData.map((node) => normalizeRoleScopeNode(scopeType, node)).filter(Boolean)
}
function normalizeRoleScopeNode(scopeType, node) {
@@ -242,16 +243,17 @@
? normalizeRoleScopeTreeData(scopeType, node.children)
: []
const metaSource = node.meta && typeof node.meta === 'object' ? node.meta : node
- const authNodes = scopeType === 'menu' && Array.isArray(metaSource.authList) && metaSource.authList.length
- ? metaSource.authList.map((auth, index) => ({
- id: normalizeScopeKey(auth.id ?? auth.authMark ?? `${node.id || 'auth'}-${index}`),
- label: normalizeScopeTitle(auth.title || auth.name || auth.authMark || ''),
- type: 1,
- isAuthButton: true,
- authMark: auth.authMark || auth.authority || auth.code || '',
- children: []
- }))
- : []
+ const authNodes =
+ scopeType === 'menu' && Array.isArray(metaSource.authList) && metaSource.authList.length
+ ? metaSource.authList.map((auth, index) => ({
+ id: normalizeScopeKey(auth.id ?? auth.authMark ?? `${node.id || 'auth'}-${index}`),
+ label: normalizeScopeTitle(auth.title || auth.name || auth.authMark || ''),
+ type: 1,
+ isAuthButton: true,
+ authMark: auth.authMark || auth.authority || auth.code || '',
+ children: []
+ }))
+ : []
const mergedChildren =
authNodes.length > 0 && !children.some((child) => child.isAuthButton)
@@ -291,11 +293,7 @@
}
return Array.from(
- new Set(
- keys
- .map((key) => normalizeRoleId(key))
- .filter((key) => key !== void 0)
- )
+ new Set(keys.map((key) => normalizeRoleId(key)).filter((key) => key !== void 0))
)
}
--
Gitblit v1.9.1