mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-03 03:00:15 +08:00
feat: update dashboard
This commit is contained in:
@@ -151,7 +151,7 @@ class AstrBotDashboard:
|
||||
@self.app.route("/")
|
||||
async def index():
|
||||
if not self.enable_webui:
|
||||
return "WebUI is disabled."
|
||||
return "Buildin WebUI is disabled."
|
||||
try:
|
||||
return await self.app.send_static_file("index.html")
|
||||
except werkzeug.exceptions.NotFound:
|
||||
@@ -161,7 +161,7 @@ class AstrBotDashboard:
|
||||
@self.app.errorhandler(404)
|
||||
async def not_found(e):
|
||||
if not self.enable_webui:
|
||||
return "WebUI is disabled."
|
||||
return "Buildin WebUI is disabled."
|
||||
if request.path.startswith("/api/"):
|
||||
return jsonify(Response().error("Not Found").to_json()), 404
|
||||
try:
|
||||
|
||||
29
dashboard/.eslintignore
Normal file
29
dashboard/.eslintignore
Normal file
@@ -0,0 +1,29 @@
|
||||
# ESLint ignore file for AstrBot dashboard
|
||||
# Skip dependency directories and build artifacts
|
||||
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
public/
|
||||
coverage/
|
||||
.vite/
|
||||
.cache/
|
||||
*.min.js
|
||||
*.bundle.js
|
||||
*.map
|
||||
|
||||
# Dashboard-specific artifacts (when lint is run from repo root using --prefix)
|
||||
dashboard/dist/
|
||||
dashboard/node_modules/
|
||||
|
||||
# Generated TypeScript declaration used by environment - can cause parser issues in some setups
|
||||
env.d.ts
|
||||
|
||||
# Scripts and tooling files (often use newer syntax or are run in node env)
|
||||
scripts/**/*.mjs
|
||||
scripts/**/*.cjs
|
||||
|
||||
# Misc
|
||||
*.log
|
||||
.idea/
|
||||
.vscode/
|
||||
149
dashboard/.eslintrc.cjs
Normal file
149
dashboard/.eslintrc.cjs
Normal file
@@ -0,0 +1,149 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
es2021: true,
|
||||
},
|
||||
|
||||
// Use vue-eslint-parser so .vue SFCs are parsed correctly.
|
||||
parser: "vue-eslint-parser",
|
||||
|
||||
parserOptions: {
|
||||
// vue-eslint-parser will forward the script content to this parser
|
||||
parser: "@typescript-eslint/parser",
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
extraFileExtensions: [".vue"],
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
// NOTE: Intentionally NO `project` here to avoid requiring type-aware linting.
|
||||
// This keeps eslint fast and avoids the TSConfig inclusion errors.
|
||||
},
|
||||
|
||||
plugins: ["vue", "@typescript-eslint"],
|
||||
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:vue/vue3-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
// Intentionally not extending type-aware or prettier-requiring configs.
|
||||
],
|
||||
|
||||
settings: {
|
||||
// Allow using Vue compiler macros like defineProps/defineEmits in templates
|
||||
"vue/setup-compiler-macros": true,
|
||||
},
|
||||
|
||||
// Avoid linting build artifacts and generated files
|
||||
ignorePatterns: [
|
||||
"dist/",
|
||||
"build/",
|
||||
"node_modules/",
|
||||
"public/",
|
||||
"dashboard/dist/",
|
||||
"dashboard/node_modules/",
|
||||
"env.d.ts",
|
||||
"scripts/**/*.mjs",
|
||||
".vite/",
|
||||
".cache/",
|
||||
],
|
||||
|
||||
rules: {
|
||||
// Keep console/debug permissible but warned
|
||||
"no-console": ["warn", { allow: ["warn", "error", "info"] }],
|
||||
"no-debugger": "warn",
|
||||
|
||||
// TypeScript rules (relaxed)
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
caughtErrorsIgnorePattern: "^_",
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
|
||||
// Vue rules adjustments — relax a few rules that generate a lot of noise
|
||||
// These are intentionally relaxed to allow incremental, safe fixes of template code.
|
||||
"vue/multi-word-component-names": "off",
|
||||
"vue/html-self-closing": [
|
||||
"error",
|
||||
{
|
||||
html: {
|
||||
void: "never",
|
||||
normal: "always",
|
||||
component: "always",
|
||||
},
|
||||
svg: "always",
|
||||
math: "always",
|
||||
},
|
||||
],
|
||||
|
||||
// Reduce template noise for legacy / Vuetify patterns used across this codebase
|
||||
"vue/valid-v-slot": "off",
|
||||
"vue/v-on-event-hyphenation": "off",
|
||||
"vue/no-unused-components": "off",
|
||||
// Broadly disable unused vars detection for templates to avoid false positives from compiled/generated template usage
|
||||
"vue/no-unused-vars": "off",
|
||||
"vue/require-default-prop": "off",
|
||||
// Keep v-html as a warn so security-sensitive usage is highlighted
|
||||
"vue/no-v-html": "warn",
|
||||
},
|
||||
|
||||
overrides: [
|
||||
// Vue Single File Components
|
||||
{
|
||||
files: ["*.vue", "src/**/*.vue"],
|
||||
parser: "vue-eslint-parser",
|
||||
parserOptions: {
|
||||
parser: "@typescript-eslint/parser",
|
||||
extraFileExtensions: [".vue"],
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
// Enable type-aware rules for script blocks inside .vue files
|
||||
project: "./tsconfig.eslint.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
rules: {
|
||||
// Component/template specific overrides can go here
|
||||
},
|
||||
},
|
||||
|
||||
// TypeScript files (no project required)
|
||||
{
|
||||
files: ["*.ts", "*.tsx", "src/**/*.ts", "src/**/*.tsx"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
// Use type-aware linting for TS files via the dedicated tsconfig for ESLint
|
||||
project: "./tsconfig.eslint.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
rules: {
|
||||
// Project-specific relaxations for TS files
|
||||
},
|
||||
},
|
||||
|
||||
// Node scripts / tooling
|
||||
{
|
||||
files: ["scripts/**/*.mjs", "scripts/**/*.cjs", "*.cjs"],
|
||||
env: { node: true },
|
||||
parserOptions: { sourceType: "module" },
|
||||
},
|
||||
// Disable strict v-slot validation for extension component panels where shorthand slots are used
|
||||
{
|
||||
files: [
|
||||
"src/components/extension/componentPanel/**",
|
||||
"src/components/extension/**",
|
||||
],
|
||||
rules: {
|
||||
"vue/valid-v-slot": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,14 +1,28 @@
|
||||
<template>
|
||||
<RouterView></RouterView>
|
||||
<RouterView />
|
||||
<WaitingForRestart ref="globalWaitingRef" />
|
||||
|
||||
<!-- 全局唯一 snackbar -->
|
||||
<v-snackbar v-if="toastStore.current" v-model="snackbarShow" :color="toastStore.current.color"
|
||||
:timeout="toastStore.current.timeout" :multi-line="toastStore.current.multiLine"
|
||||
:location="toastStore.current.location" close-on-back>
|
||||
<v-snackbar
|
||||
v-if="toastStore.current"
|
||||
v-model="snackbarShow"
|
||||
:color="toastStore.current.color"
|
||||
:timeout="toastStore.current.timeout"
|
||||
:multi-line="toastStore.current.multiLine"
|
||||
:location="toastStore.current.location"
|
||||
close-on-back
|
||||
>
|
||||
{{ toastStore.current.message }}
|
||||
<template #actions v-if="toastStore.current.closable">
|
||||
<v-btn variant="text" @click="snackbarShow = false">关闭</v-btn>
|
||||
<template
|
||||
v-if="toastStore.current.closable"
|
||||
#actions
|
||||
>
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="snackbarShow = false"
|
||||
>
|
||||
关闭
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</template>
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
<template>
|
||||
<v-dialog v-model="isOpen" max-width="400">
|
||||
<v-dialog
|
||||
v-model="isOpen"
|
||||
max-width="400"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="text-h6">{{ title }}</v-card-title>
|
||||
<v-card-title class="text-h6">
|
||||
{{ title }}
|
||||
</v-card-title>
|
||||
<v-card-text>{{ message }}</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="gray" @click="handleCancel">{{ t('core.common.dialog.cancelButton') }}</v-btn>
|
||||
<v-btn color="red" @click="handleConfirm">{{ t('core.common.dialog.confirmButton') }}</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="gray"
|
||||
@click="handleCancel"
|
||||
>
|
||||
{{ t('core.common.dialog.cancelButton') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="red"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
{{ t('core.common.dialog.confirmButton') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
@@ -1,208 +1,279 @@
|
||||
<template>
|
||||
<v-card class="chat-page-card" elevation="0" rounded="0">
|
||||
<v-card-text class="chat-page-container">
|
||||
<!-- 遮罩层 (手机端) -->
|
||||
<div class="mobile-overlay" v-if="isMobile && mobileMenuOpen" @click="closeMobileSidebar"></div>
|
||||
<v-card
|
||||
class="chat-page-card"
|
||||
elevation="0"
|
||||
rounded="0"
|
||||
>
|
||||
<v-card-text class="chat-page-container">
|
||||
<!-- 遮罩层 (手机端) -->
|
||||
<div
|
||||
v-if="isMobile && mobileMenuOpen"
|
||||
class="mobile-overlay"
|
||||
@click="closeMobileSidebar"
|
||||
/>
|
||||
|
||||
<div class="chat-layout">
|
||||
<ConversationSidebar
|
||||
:sessions="sessions"
|
||||
:selectedSessions="selectedSessions"
|
||||
:currSessionId="currSessionId"
|
||||
:selectedProjectId="selectedProjectId"
|
||||
:transportMode="transportMode"
|
||||
:sendShortcut="sendShortcut"
|
||||
:isDark="isDark"
|
||||
:chatboxMode="chatboxMode"
|
||||
:isMobile="isMobile"
|
||||
:mobileMenuOpen="mobileMenuOpen"
|
||||
:projects="projects"
|
||||
@newChat="handleNewChat"
|
||||
@selectConversation="handleSelectConversation"
|
||||
@editTitle="showEditTitleDialog"
|
||||
@deleteConversation="handleDeleteConversation"
|
||||
@batchDeleteConversations="handleBatchDeleteConversations"
|
||||
@closeMobileSidebar="closeMobileSidebar"
|
||||
@toggleTheme="toggleTheme"
|
||||
@toggleFullscreen="toggleFullscreen"
|
||||
@selectProject="handleSelectProject"
|
||||
@createProject="showCreateProjectDialog"
|
||||
@editProject="showEditProjectDialog"
|
||||
@deleteProject="handleDeleteProject"
|
||||
@updateTransportMode="setTransportMode"
|
||||
@updateSendShortcut="setSendShortcut"
|
||||
/>
|
||||
<div class="chat-layout">
|
||||
<ConversationSidebar
|
||||
:sessions="sessions"
|
||||
:selected-sessions="selectedSessions"
|
||||
:curr-session-id="currSessionId"
|
||||
:selected-project-id="selectedProjectId"
|
||||
:transport-mode="transportMode"
|
||||
:send-shortcut="sendShortcut"
|
||||
:is-dark="isDark"
|
||||
:chatbox-mode="chatboxMode"
|
||||
:is-mobile="isMobile"
|
||||
:mobile-menu-open="mobileMenuOpen"
|
||||
:projects="projects"
|
||||
@newChat="handleNewChat"
|
||||
@selectConversation="handleSelectConversation"
|
||||
@editTitle="showEditTitleDialog"
|
||||
@deleteConversation="handleDeleteConversation"
|
||||
@batchDeleteConversations="handleBatchDeleteConversations"
|
||||
@closeMobileSidebar="closeMobileSidebar"
|
||||
@toggleTheme="toggleTheme"
|
||||
@toggleFullscreen="toggleFullscreen"
|
||||
@selectProject="handleSelectProject"
|
||||
@createProject="showCreateProjectDialog"
|
||||
@editProject="showEditProjectDialog"
|
||||
@deleteProject="handleDeleteProject"
|
||||
@updateTransportMode="setTransportMode"
|
||||
@updateSendShortcut="setSendShortcut"
|
||||
/>
|
||||
|
||||
<!-- 右侧聊天内容区域 -->
|
||||
<div class="chat-content-panel">
|
||||
<!-- Live Mode -->
|
||||
<LiveMode v-if="liveModeOpen" @close="closeLiveMode" />
|
||||
<!-- 右侧聊天内容区域 -->
|
||||
<div class="chat-content-panel">
|
||||
<!-- Live Mode -->
|
||||
<LiveMode
|
||||
v-if="liveModeOpen"
|
||||
@close="closeLiveMode"
|
||||
/>
|
||||
|
||||
<!-- 正常聊天界面 -->
|
||||
<template v-else>
|
||||
|
||||
<div v-if="currentSessionProject && messages && messages.length > 0" class="breadcrumb-container">
|
||||
<div class="breadcrumb-content">
|
||||
<span class="breadcrumb-emoji">{{ currentSessionProject.emoji || '📁' }}</span>
|
||||
<span class="breadcrumb-project" @click="handleSelectProject(currentSessionProject.project_id)">{{ currentSessionProject.title }}</span>
|
||||
<v-icon size="small" class="breadcrumb-separator">mdi-chevron-right</v-icon>
|
||||
<span class="breadcrumb-session">{{ getCurrentSession?.display_name || tm('conversation.newConversation') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-list-wrapper" v-if="currSessionId && !selectedProjectId">
|
||||
<MessageList :messages="messages" :isDark="isDark"
|
||||
:isStreaming="isStreaming || isConvRunning"
|
||||
:isLoadingMessages="isLoadingMessages"
|
||||
@openImagePreview="openImagePreview"
|
||||
@replyMessage="handleReplyMessage"
|
||||
@replyWithText="handleReplyWithText"
|
||||
@openRefs="handleOpenRefs"
|
||||
ref="messageList" />
|
||||
<div class="message-list-fade" :class="{ 'fade-dark': isDark }"></div>
|
||||
</div>
|
||||
<ProjectView
|
||||
v-else-if="selectedProjectId"
|
||||
:project="currentProject"
|
||||
:sessions="projectSessions"
|
||||
@selectSession="(sessionId) => handleSelectConversation([sessionId])"
|
||||
@editSessionTitle="showEditTitleDialog"
|
||||
@deleteSession="handleDeleteConversation"
|
||||
>
|
||||
<ChatInput
|
||||
v-model:prompt="prompt"
|
||||
:stagedImagesUrl="stagedImagesUrl"
|
||||
:stagedAudioUrl="stagedAudioUrl"
|
||||
:stagedFiles="stagedNonImageFiles"
|
||||
:disabled="false"
|
||||
:is-running="isStreaming || isConvRunning"
|
||||
:enableStreaming="enableStreaming"
|
||||
:isRecording="isRecording"
|
||||
:session-id="currSessionId || null"
|
||||
:current-session="getCurrentSession"
|
||||
:replyTo="replyTo"
|
||||
:send-shortcut="sendShortcut"
|
||||
@send="handleSendMessage"
|
||||
@stop="handleStopMessage"
|
||||
@toggleStreaming="toggleStreaming"
|
||||
@removeImage="removeImage"
|
||||
@removeAudio="removeAudio"
|
||||
@removeFile="removeFile"
|
||||
@startRecording="handleStartRecording"
|
||||
@stopRecording="handleStopRecording"
|
||||
@pasteImage="handlePaste"
|
||||
@fileSelect="handleFileSelect"
|
||||
@clearReply="clearReply"
|
||||
@openLiveMode="openLiveMode"
|
||||
ref="chatInputRef"
|
||||
/>
|
||||
</ProjectView>
|
||||
<WelcomeView
|
||||
v-else
|
||||
:isLoading="isLoadingMessages"
|
||||
>
|
||||
<ChatInput
|
||||
v-model:prompt="prompt"
|
||||
:stagedImagesUrl="stagedImagesUrl"
|
||||
:stagedAudioUrl="stagedAudioUrl"
|
||||
:stagedFiles="stagedNonImageFiles"
|
||||
:disabled="false"
|
||||
:is-running="isStreaming || isConvRunning"
|
||||
:enableStreaming="enableStreaming"
|
||||
:isRecording="isRecording"
|
||||
:session-id="currSessionId || null"
|
||||
:current-session="getCurrentSession"
|
||||
:replyTo="replyTo"
|
||||
:send-shortcut="sendShortcut"
|
||||
@send="handleSendMessage"
|
||||
@stop="handleStopMessage"
|
||||
@toggleStreaming="toggleStreaming"
|
||||
@removeImage="removeImage"
|
||||
@removeAudio="removeAudio"
|
||||
@removeFile="removeFile"
|
||||
@startRecording="handleStartRecording"
|
||||
@stopRecording="handleStopRecording"
|
||||
@pasteImage="handlePaste"
|
||||
@fileSelect="handleFileSelect"
|
||||
@clearReply="clearReply"
|
||||
@openLiveMode="openLiveMode"
|
||||
ref="chatInputRef"
|
||||
/>
|
||||
</WelcomeView>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<ChatInput
|
||||
v-if="currSessionId && !selectedProjectId"
|
||||
v-model:prompt="prompt"
|
||||
:stagedImagesUrl="stagedImagesUrl"
|
||||
:stagedAudioUrl="stagedAudioUrl"
|
||||
:stagedFiles="stagedNonImageFiles"
|
||||
:disabled="false"
|
||||
:is-running="isStreaming || isConvRunning"
|
||||
:enableStreaming="enableStreaming"
|
||||
:isRecording="isRecording"
|
||||
:session-id="currSessionId || null"
|
||||
:current-session="getCurrentSession"
|
||||
:replyTo="replyTo"
|
||||
:send-shortcut="sendShortcut"
|
||||
@send="handleSendMessage"
|
||||
@stop="handleStopMessage"
|
||||
@toggleStreaming="toggleStreaming"
|
||||
@removeImage="removeImage"
|
||||
@removeAudio="removeAudio"
|
||||
@removeFile="removeFile"
|
||||
@startRecording="handleStartRecording"
|
||||
@stopRecording="handleStopRecording"
|
||||
@pasteImage="handlePaste"
|
||||
@fileSelect="handleFileSelect"
|
||||
@clearReply="clearReply"
|
||||
@openLiveMode="openLiveMode"
|
||||
ref="chatInputRef"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Refs Sidebar -->
|
||||
<RefsSidebar v-model="refsSidebarOpen" :refs="refsSidebarRefs" />
|
||||
<!-- 正常聊天界面 -->
|
||||
<template v-else>
|
||||
<div
|
||||
v-if="currentSessionProject && messages && messages.length > 0"
|
||||
class="breadcrumb-container"
|
||||
>
|
||||
<div class="breadcrumb-content">
|
||||
<span class="breadcrumb-emoji">{{ currentSessionProject.emoji || '📁' }}</span>
|
||||
<span
|
||||
class="breadcrumb-project"
|
||||
@click="handleSelectProject(currentSessionProject.project_id)"
|
||||
>{{ currentSessionProject.title }}</span>
|
||||
<v-icon
|
||||
size="small"
|
||||
class="breadcrumb-separator"
|
||||
>
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
<span class="breadcrumb-session">{{ getCurrentSession?.display_name || tm('conversation.newConversation') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<div
|
||||
v-if="currSessionId && !selectedProjectId"
|
||||
class="message-list-wrapper"
|
||||
>
|
||||
<MessageList
|
||||
ref="messageList"
|
||||
:messages="messages"
|
||||
:is-dark="isDark"
|
||||
:is-streaming="isStreaming || isConvRunning"
|
||||
:is-loading-messages="isLoadingMessages"
|
||||
@openImagePreview="openImagePreview"
|
||||
@replyMessage="handleReplyMessage"
|
||||
@replyWithText="handleReplyWithText"
|
||||
@openRefs="handleOpenRefs"
|
||||
/>
|
||||
<div
|
||||
class="message-list-fade"
|
||||
:class="{ 'fade-dark': isDark }"
|
||||
/>
|
||||
</div>
|
||||
<ProjectView
|
||||
v-else-if="selectedProjectId"
|
||||
:project="currentProject"
|
||||
:sessions="projectSessions"
|
||||
@selectSession="(sessionId) => handleSelectConversation([sessionId])"
|
||||
@editSessionTitle="showEditTitleDialog"
|
||||
@deleteSession="handleDeleteConversation"
|
||||
>
|
||||
<ChatInput
|
||||
ref="chatInputRef"
|
||||
v-model:prompt="prompt"
|
||||
:staged-images-url="stagedImagesUrl"
|
||||
:staged-audio-url="stagedAudioUrl"
|
||||
:staged-files="stagedNonImageFiles"
|
||||
:disabled="false"
|
||||
:is-running="isStreaming || isConvRunning"
|
||||
:enable-streaming="enableStreaming"
|
||||
:is-recording="isRecording"
|
||||
:session-id="currSessionId || null"
|
||||
:current-session="getCurrentSession"
|
||||
:reply-to="replyTo"
|
||||
:send-shortcut="sendShortcut"
|
||||
@send="handleSendMessage"
|
||||
@stop="handleStopMessage"
|
||||
@toggleStreaming="toggleStreaming"
|
||||
@removeImage="removeImage"
|
||||
@removeAudio="removeAudio"
|
||||
@removeFile="removeFile"
|
||||
@startRecording="handleStartRecording"
|
||||
@stopRecording="handleStopRecording"
|
||||
@pasteImage="handlePaste"
|
||||
@fileSelect="handleFileSelect"
|
||||
@clearReply="clearReply"
|
||||
@openLiveMode="openLiveMode"
|
||||
/>
|
||||
</ProjectView>
|
||||
<WelcomeView
|
||||
v-else
|
||||
:is-loading="isLoadingMessages"
|
||||
>
|
||||
<ChatInput
|
||||
ref="chatInputRef"
|
||||
v-model:prompt="prompt"
|
||||
:staged-images-url="stagedImagesUrl"
|
||||
:staged-audio-url="stagedAudioUrl"
|
||||
:staged-files="stagedNonImageFiles"
|
||||
:disabled="false"
|
||||
:is-running="isStreaming || isConvRunning"
|
||||
:enable-streaming="enableStreaming"
|
||||
:is-recording="isRecording"
|
||||
:session-id="currSessionId || null"
|
||||
:current-session="getCurrentSession"
|
||||
:reply-to="replyTo"
|
||||
:send-shortcut="sendShortcut"
|
||||
@send="handleSendMessage"
|
||||
@stop="handleStopMessage"
|
||||
@toggleStreaming="toggleStreaming"
|
||||
@removeImage="removeImage"
|
||||
@removeAudio="removeAudio"
|
||||
@removeFile="removeFile"
|
||||
@startRecording="handleStartRecording"
|
||||
@stopRecording="handleStopRecording"
|
||||
@pasteImage="handlePaste"
|
||||
@fileSelect="handleFileSelect"
|
||||
@clearReply="clearReply"
|
||||
@openLiveMode="openLiveMode"
|
||||
/>
|
||||
</WelcomeView>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<ChatInput
|
||||
v-if="currSessionId && !selectedProjectId"
|
||||
ref="chatInputRef"
|
||||
v-model:prompt="prompt"
|
||||
:staged-images-url="stagedImagesUrl"
|
||||
:staged-audio-url="stagedAudioUrl"
|
||||
:staged-files="stagedNonImageFiles"
|
||||
:disabled="false"
|
||||
:is-running="isStreaming || isConvRunning"
|
||||
:enable-streaming="enableStreaming"
|
||||
:is-recording="isRecording"
|
||||
:session-id="currSessionId || null"
|
||||
:current-session="getCurrentSession"
|
||||
:reply-to="replyTo"
|
||||
:send-shortcut="sendShortcut"
|
||||
@send="handleSendMessage"
|
||||
@stop="handleStopMessage"
|
||||
@toggleStreaming="toggleStreaming"
|
||||
@removeImage="removeImage"
|
||||
@removeAudio="removeAudio"
|
||||
@removeFile="removeFile"
|
||||
@startRecording="handleStartRecording"
|
||||
@stopRecording="handleStopRecording"
|
||||
@pasteImage="handlePaste"
|
||||
@fileSelect="handleFileSelect"
|
||||
@clearReply="clearReply"
|
||||
@openLiveMode="openLiveMode"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Refs Sidebar -->
|
||||
<RefsSidebar
|
||||
v-model="refsSidebarOpen"
|
||||
:refs="refsSidebarRefs"
|
||||
/>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 编辑对话标题对话框 -->
|
||||
<v-dialog v-model="editTitleDialog" max-width="400">
|
||||
<v-card>
|
||||
<v-card-title class="dialog-title">{{ tm('actions.editTitle') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field v-model="editingTitle" :label="tm('conversation.newConversation')" variant="outlined"
|
||||
hide-details class="mt-2" @keyup.enter="handleSaveTitle" autofocus />
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="editTitleDialog = false" color="grey-darken-1">{{ t('core.common.cancel') }}</v-btn>
|
||||
<v-btn variant="text" @click="handleSaveTitle" color="primary">{{ t('core.common.save') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<!-- 编辑对话标题对话框 -->
|
||||
<v-dialog
|
||||
v-model="editTitleDialog"
|
||||
max-width="400"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="dialog-title">
|
||||
{{ tm('actions.editTitle') }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="editingTitle"
|
||||
:label="tm('conversation.newConversation')"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
class="mt-2"
|
||||
autofocus
|
||||
@keyup.enter="handleSaveTitle"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
color="grey-darken-1"
|
||||
@click="editTitleDialog = false"
|
||||
>
|
||||
{{ t('core.common.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="text"
|
||||
color="primary"
|
||||
@click="handleSaveTitle"
|
||||
>
|
||||
{{ t('core.common.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 图片预览对话框 -->
|
||||
<v-dialog v-model="imagePreviewDialog" max-width="90vw" max-height="90vh">
|
||||
<v-card class="image-preview-card" elevation="8">
|
||||
<v-card-title class="d-flex justify-space-between align-center pa-4">
|
||||
<span>{{ t('core.common.imagePreview') }}</span>
|
||||
<v-btn icon="mdi-close" variant="text" @click="imagePreviewDialog = false" />
|
||||
</v-card-title>
|
||||
<v-card-text class="text-center pa-4">
|
||||
<img :src="previewImageUrl" class="preview-image-large" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<!-- 图片预览对话框 -->
|
||||
<v-dialog
|
||||
v-model="imagePreviewDialog"
|
||||
max-width="90vw"
|
||||
max-height="90vh"
|
||||
>
|
||||
<v-card
|
||||
class="image-preview-card"
|
||||
elevation="8"
|
||||
>
|
||||
<v-card-title class="d-flex justify-space-between align-center pa-4">
|
||||
<span>{{ t('core.common.imagePreview') }}</span>
|
||||
<v-btn
|
||||
icon="mdi-close"
|
||||
variant="text"
|
||||
@click="imagePreviewDialog = false"
|
||||
/>
|
||||
</v-card-title>
|
||||
<v-card-text class="text-center pa-4">
|
||||
<img
|
||||
:src="previewImageUrl"
|
||||
class="preview-image-large"
|
||||
>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 创建/编辑项目对话框 -->
|
||||
<ProjectDialog
|
||||
v-model="projectDialog"
|
||||
:project="editingProject"
|
||||
@save="handleSaveProject"
|
||||
/>
|
||||
<!-- 创建/编辑项目对话框 -->
|
||||
<ProjectDialog
|
||||
v-model="projectDialog"
|
||||
:project="editingProject"
|
||||
@save="handleSaveProject"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,82 +1,166 @@
|
||||
<template>
|
||||
<div class="input-area fade-in" @dragover.prevent="handleDragOver" @dragleave.prevent="handleDragLeave"
|
||||
@drop.prevent="handleDrop">
|
||||
<div class="input-container" :style="{
|
||||
width: '85%',
|
||||
maxWidth: '900px',
|
||||
margin: '0 auto',
|
||||
border: isDark ? 'none' : '1px solid #e0e0e0',
|
||||
borderRadius: '24px',
|
||||
boxShadow: isDark ? 'none' : '0px 2px 2px rgba(0, 0, 0, 0.1)',
|
||||
backgroundColor: isDark ? '#2d2d2d' : 'transparent',
|
||||
position: 'relative'
|
||||
}">
|
||||
<!-- 拖拽上传遮罩 -->
|
||||
<transition name="fade">
|
||||
<div v-if="isDragging" class="drop-overlay">
|
||||
<div class="drop-overlay-content">
|
||||
<v-icon size="48" color="primary">mdi-cloud-upload</v-icon>
|
||||
<span class="drop-text">{{ tm('input.dropToUpload') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<!-- 引用预览区 -->
|
||||
<transition name="slideReply" @after-leave="handleReplyAfterLeave">
|
||||
<div class="reply-preview" v-if="props.replyTo && !isReplyClosing">
|
||||
<div class="reply-content">
|
||||
<v-icon size="small" class="reply-icon">mdi-reply</v-icon>
|
||||
"<span class="reply-text">{{ props.replyTo.selectedText }}</span>"
|
||||
</div>
|
||||
<v-btn @click="handleClearReply" class="remove-reply-btn" icon="mdi-close" size="x-small"
|
||||
color="grey" variant="text" />
|
||||
</div>
|
||||
</transition>
|
||||
<textarea ref="inputField" v-model="localPrompt" @keydown="handleKeyDown" :disabled="disabled"
|
||||
placeholder="Ask AstrBot..." class="chat-textarea"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="sentences" spellcheck="false"
|
||||
style="width: 100%; resize: none; outline: none; border: 1px solid var(--v-theme-border); border-radius: 12px; padding: 16px 20px; min-height: 40px; max-height: 200px; overflow-y: auto; font-family: inherit; font-size: 16px; background-color: var(--v-theme-surface);"></textarea>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: 6px 14px;">
|
||||
<div
|
||||
style="display: flex; justify-content: flex-start; margin-top: 4px; align-items: center; gap: 8px; min-width: 0; flex: 1; overflow: hidden;">
|
||||
<!-- Settings Menu -->
|
||||
<StyledMenu offset="8" location="top start" :close-on-content-click="false">
|
||||
<template v-slot:activator="{ props: activatorProps }">
|
||||
<v-btn v-bind="activatorProps" icon="mdi-plus" variant="text" color="primary" />
|
||||
</template>
|
||||
<div
|
||||
class="input-area fade-in"
|
||||
@dragover.prevent="handleDragOver"
|
||||
@dragleave.prevent="handleDragLeave"
|
||||
@drop.prevent="handleDrop"
|
||||
>
|
||||
<div
|
||||
class="input-container"
|
||||
:style="{
|
||||
width: '85%',
|
||||
maxWidth: '900px',
|
||||
margin: '0 auto',
|
||||
border: isDark ? 'none' : '1px solid #e0e0e0',
|
||||
borderRadius: '24px',
|
||||
boxShadow: isDark ? 'none' : '0px 2px 2px rgba(0, 0, 0, 0.1)',
|
||||
backgroundColor: isDark ? '#2d2d2d' : 'transparent',
|
||||
position: 'relative'
|
||||
}"
|
||||
>
|
||||
<!-- 拖拽上传遮罩 -->
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="isDragging"
|
||||
class="drop-overlay"
|
||||
>
|
||||
<div class="drop-overlay-content">
|
||||
<v-icon
|
||||
size="48"
|
||||
color="primary"
|
||||
>
|
||||
mdi-cloud-upload
|
||||
</v-icon>
|
||||
<span class="drop-text">{{ tm('input.dropToUpload') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<!-- 引用预览区 -->
|
||||
<transition
|
||||
name="slideReply"
|
||||
@after-leave="handleReplyAfterLeave"
|
||||
>
|
||||
<div
|
||||
v-if="props.replyTo && !isReplyClosing"
|
||||
class="reply-preview"
|
||||
>
|
||||
<div class="reply-content">
|
||||
<v-icon
|
||||
size="small"
|
||||
class="reply-icon"
|
||||
>
|
||||
mdi-reply
|
||||
</v-icon>
|
||||
"<span class="reply-text">{{ props.replyTo.selectedText }}</span>"
|
||||
</div>
|
||||
<v-btn
|
||||
class="remove-reply-btn"
|
||||
icon="mdi-close"
|
||||
size="x-small"
|
||||
color="grey"
|
||||
variant="text"
|
||||
@click="handleClearReply"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
<textarea
|
||||
ref="inputField"
|
||||
v-model="localPrompt"
|
||||
:disabled="disabled"
|
||||
placeholder="Ask AstrBot..."
|
||||
class="chat-textarea"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="sentences"
|
||||
spellcheck="false"
|
||||
style="width: 100%; resize: none; outline: none; border: 1px solid var(--v-theme-border); border-radius: 12px; padding: 16px 20px; min-height: 40px; max-height: 200px; overflow-y: auto; font-family: inherit; font-size: 16px; background-color: var(--v-theme-surface);"
|
||||
@keydown="handleKeyDown"
|
||||
/>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: 6px 14px;">
|
||||
<div
|
||||
style="display: flex; justify-content: flex-start; margin-top: 4px; align-items: center; gap: 8px; min-width: 0; flex: 1; overflow: hidden;"
|
||||
>
|
||||
<!-- Settings Menu -->
|
||||
<StyledMenu
|
||||
offset="8"
|
||||
location="top start"
|
||||
:close-on-content-click="false"
|
||||
>
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-btn
|
||||
v-bind="activatorProps"
|
||||
icon="mdi-plus"
|
||||
variant="text"
|
||||
color="primary"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Upload Files -->
|
||||
<v-list-item class="styled-menu-item" rounded="md" @click="triggerImageInput">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-file-upload-outline" size="small"></v-icon>
|
||||
</template>
|
||||
<v-list-item-title>
|
||||
{{ tm('input.upload') }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<!-- Upload Files -->
|
||||
<v-list-item
|
||||
class="styled-menu-item"
|
||||
rounded="md"
|
||||
@click="triggerImageInput"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
icon="mdi-file-upload-outline"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
<v-list-item-title>
|
||||
{{ tm('input.upload') }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<!-- Config Selector in Menu -->
|
||||
<ConfigSelector :session-id="sessionId || null" :platform-id="sessionPlatformId"
|
||||
:is-group="sessionIsGroup" :initial-config-id="props.configId"
|
||||
@config-changed="handleConfigChange" />
|
||||
<!-- Config Selector in Menu -->
|
||||
<ConfigSelector
|
||||
:session-id="sessionId || null"
|
||||
:platform-id="sessionPlatformId"
|
||||
:is-group="sessionIsGroup"
|
||||
:initial-config-id="props.configId"
|
||||
@config-changed="handleConfigChange"
|
||||
/>
|
||||
|
||||
<!-- Streaming Toggle in Menu -->
|
||||
<v-list-item class="styled-menu-item" rounded="md" @click="$emit('toggleStreaming')">
|
||||
<template v-slot:prepend>
|
||||
<v-icon :icon="enableStreaming ? 'mdi-flash' : 'mdi-flash-off'" size="small"></v-icon>
|
||||
</template>
|
||||
<v-list-item-title>
|
||||
{{ enableStreaming ? tm('streaming.enabled') : tm('streaming.disabled') }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</StyledMenu>
|
||||
<!-- Streaming Toggle in Menu -->
|
||||
<v-list-item
|
||||
class="styled-menu-item"
|
||||
rounded="md"
|
||||
@click="$emit('toggleStreaming')"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
:icon="enableStreaming ? 'mdi-flash' : 'mdi-flash-off'"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
<v-list-item-title>
|
||||
{{ enableStreaming ? tm('streaming.enabled') : tm('streaming.disabled') }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</StyledMenu>
|
||||
|
||||
<!-- Provider/Model Selector Menu -->
|
||||
<ProviderModelMenu v-if="showProviderSelector" ref="providerModelMenuRef" />
|
||||
</div>
|
||||
<div style="display: flex; justify-content: flex-end; margin-top: 8px; align-items: center; flex-shrink: 0;">
|
||||
<input type="file" ref="imageInputRef" @change="handleFileSelect" style="display: none" multiple />
|
||||
<v-progress-circular v-if="disabled && !mobile" indeterminate size="16" class="mr-1" width="1.5" />
|
||||
<!-- <v-btn @click="$emit('openLiveMode')"
|
||||
<!-- Provider/Model Selector Menu -->
|
||||
<ProviderModelMenu
|
||||
v-if="showProviderSelector"
|
||||
ref="providerModelMenuRef"
|
||||
/>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: flex-end; margin-top: 8px; align-items: center; flex-shrink: 0;">
|
||||
<input
|
||||
ref="imageInputRef"
|
||||
type="file"
|
||||
style="display: none"
|
||||
multiple
|
||||
@change="handleFileSelect"
|
||||
>
|
||||
<v-progress-circular
|
||||
v-if="disabled && !mobile"
|
||||
indeterminate
|
||||
size="16"
|
||||
class="mr-1"
|
||||
width="1.5"
|
||||
/>
|
||||
<!-- <v-btn @click="$emit('openLiveMode')"
|
||||
icon
|
||||
variant="text"
|
||||
color="purple"
|
||||
@@ -87,54 +171,136 @@
|
||||
{{ tm('voice.liveMode') }}
|
||||
</v-tooltip>
|
||||
</v-btn> -->
|
||||
<v-btn @click="handleRecordClick" icon variant="text" :color="isRecording ? 'error' : 'primary'"
|
||||
class="record-btn">
|
||||
<v-icon :icon="isRecording ? 'mdi-stop-circle' : 'mdi-microphone'" variant="text"
|
||||
plain></v-icon>
|
||||
<v-tooltip activator="parent" location="top">
|
||||
{{ isRecording ? tm('voice.speaking') : tm('voice.startRecording') }}
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn icon v-if="isRunning && !canSend" @click="$emit('stop')" variant="tonal" color="primary" class="send-btn">
|
||||
<v-icon icon="mdi-stop" variant="text" plain></v-icon>
|
||||
<v-tooltip activator="parent" location="top">
|
||||
{{ tm('input.stopGenerating') }}
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn v-else @click="$emit('send')" icon="mdi-send" variant="tonal" color="primary"
|
||||
:disabled="!canSend" class="send-btn" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 附件预览区 -->
|
||||
<div class="attachments-preview"
|
||||
v-if="stagedImagesUrl.length > 0 || stagedAudioUrl || (stagedFiles && stagedFiles.length > 0)">
|
||||
<div v-for="(img, index) in stagedImagesUrl" :key="'img-' + index" class="image-preview">
|
||||
<img :src="img" class="preview-image" />
|
||||
<v-btn @click="$emit('removeImage', index)" class="remove-attachment-btn" icon="mdi-close" size="small"
|
||||
color="error" variant="text" />
|
||||
</div>
|
||||
|
||||
<div v-if="stagedAudioUrl" class="audio-preview">
|
||||
<v-chip color="primary" variant="tonal" class="audio-chip">
|
||||
<v-icon start icon="mdi-microphone" size="small"></v-icon>
|
||||
{{ tm('voice.recording') }}
|
||||
</v-chip>
|
||||
<v-btn @click="$emit('removeAudio')" class="remove-attachment-btn" icon="mdi-close" size="small"
|
||||
color="error" variant="text" />
|
||||
</div>
|
||||
|
||||
<div v-for="(file, index) in stagedFiles" :key="'file-' + index" class="file-preview">
|
||||
<v-chip color="primary" variant="tonal" class="file-chip">
|
||||
<v-icon start icon="mdi-file-document-outline" size="small"></v-icon>
|
||||
<span class="file-name-preview">{{ file.original_name }}</span>
|
||||
</v-chip>
|
||||
<v-btn @click="$emit('removeFile', index)" class="remove-attachment-btn" icon="mdi-close" size="small"
|
||||
color="error" variant="text" />
|
||||
</div>
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
:color="isRecording ? 'error' : 'primary'"
|
||||
class="record-btn"
|
||||
@click="handleRecordClick"
|
||||
>
|
||||
<v-icon
|
||||
:icon="isRecording ? 'mdi-stop-circle' : 'mdi-microphone'"
|
||||
variant="text"
|
||||
plain
|
||||
/>
|
||||
<v-tooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
>
|
||||
{{ isRecording ? tm('voice.speaking') : tm('voice.startRecording') }}
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="isRunning && !canSend"
|
||||
icon
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
class="send-btn"
|
||||
@click="$emit('stop')"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-stop"
|
||||
variant="text"
|
||||
plain
|
||||
/>
|
||||
<v-tooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
>
|
||||
{{ tm('input.stopGenerating') }}
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
icon="mdi-send"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
:disabled="!canSend"
|
||||
class="send-btn"
|
||||
@click="$emit('send')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 附件预览区 -->
|
||||
<div
|
||||
v-if="stagedImagesUrl.length > 0 || stagedAudioUrl || (stagedFiles && stagedFiles.length > 0)"
|
||||
class="attachments-preview"
|
||||
>
|
||||
<div
|
||||
v-for="(img, index) in stagedImagesUrl"
|
||||
:key="'img-' + index"
|
||||
class="image-preview"
|
||||
>
|
||||
<img
|
||||
:src="img"
|
||||
class="preview-image"
|
||||
>
|
||||
<v-btn
|
||||
class="remove-attachment-btn"
|
||||
icon="mdi-close"
|
||||
size="small"
|
||||
color="error"
|
||||
variant="text"
|
||||
@click="$emit('removeImage', index)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="stagedAudioUrl"
|
||||
class="audio-preview"
|
||||
>
|
||||
<v-chip
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
class="audio-chip"
|
||||
>
|
||||
<v-icon
|
||||
start
|
||||
icon="mdi-microphone"
|
||||
size="small"
|
||||
/>
|
||||
{{ tm('voice.recording') }}
|
||||
</v-chip>
|
||||
<v-btn
|
||||
class="remove-attachment-btn"
|
||||
icon="mdi-close"
|
||||
size="small"
|
||||
color="error"
|
||||
variant="text"
|
||||
@click="$emit('removeAudio')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(file, index) in stagedFiles"
|
||||
:key="'file-' + index"
|
||||
class="file-preview"
|
||||
>
|
||||
<v-chip
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
class="file-chip"
|
||||
>
|
||||
<v-icon
|
||||
start
|
||||
icon="mdi-file-document-outline"
|
||||
size="small"
|
||||
/>
|
||||
<span class="file-name-preview">{{ file.original_name }}</span>
|
||||
</v-chip>
|
||||
<v-btn
|
||||
class="remove-attachment-btn"
|
||||
icon="mdi-close"
|
||||
size="small"
|
||||
color="error"
|
||||
variant="text"
|
||||
@click="$emit('removeFile', index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,75 +1,112 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-list-item
|
||||
class="styled-menu-item"
|
||||
rounded="md"
|
||||
@click="openDialog"
|
||||
:disabled="loadingConfigs || saving"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-cog-outline" size="small"></v-icon>
|
||||
</template>
|
||||
<v-list-item-title>
|
||||
{{ tm('config.title') }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-caption">
|
||||
{{ selectedConfigLabel }}
|
||||
</v-list-item-subtitle>
|
||||
<template v-slot:append>
|
||||
<v-icon icon="mdi-chevron-right" size="small" class="text-medium-emphasis"></v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<div>
|
||||
<v-list-item
|
||||
class="styled-menu-item"
|
||||
rounded="md"
|
||||
:disabled="loadingConfigs || saving"
|
||||
@click="openDialog"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
icon="mdi-cog-outline"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
<v-list-item-title>
|
||||
{{ tm('config.title') }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-caption">
|
||||
{{ selectedConfigLabel }}
|
||||
</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-icon
|
||||
icon="mdi-chevron-right"
|
||||
size="small"
|
||||
class="text-medium-emphasis"
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-dialog v-model="dialog" max-width="480">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center justify-space-between">
|
||||
<span>选择配置文件</span>
|
||||
<v-btn icon variant="text" @click="closeDialog">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<div v-if="loadingConfigs" class="text-center py-6">
|
||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||
</div>
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
max-width="480"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center justify-space-between">
|
||||
<span>选择配置文件</span>
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
@click="closeDialog"
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<div
|
||||
v-if="loadingConfigs"
|
||||
class="text-center py-6"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<v-list v-else class="config-list" density="comfortable">
|
||||
<v-list-item
|
||||
v-for="config in configOptions"
|
||||
:key="config.id"
|
||||
:active="tempSelectedConfig === config.id"
|
||||
rounded="lg"
|
||||
variant="text"
|
||||
@click="tempSelectedConfig = config.id"
|
||||
>
|
||||
<v-list-item-title>{{ config.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-caption text-grey">
|
||||
{{ config.id }}
|
||||
</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-icon v-if="tempSelectedConfig === config.id" color="primary">mdi-check</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<div v-if="configOptions.length === 0" class="text-center text-body-2 text-medium-emphasis">
|
||||
暂无可选配置,请先在配置页创建。
|
||||
</div>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="closeDialog">取消</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="confirmSelection"
|
||||
:disabled="!tempSelectedConfig"
|
||||
:loading="saving"
|
||||
>
|
||||
应用
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
<v-list
|
||||
v-else
|
||||
class="config-list"
|
||||
density="comfortable"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="config in configOptions"
|
||||
:key="config.id"
|
||||
:active="tempSelectedConfig === config.id"
|
||||
rounded="lg"
|
||||
variant="text"
|
||||
@click="tempSelectedConfig = config.id"
|
||||
>
|
||||
<v-list-item-title>{{ config.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-caption text-grey">
|
||||
{{ config.id }}
|
||||
</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-icon
|
||||
v-if="tempSelectedConfig === config.id"
|
||||
color="primary"
|
||||
>
|
||||
mdi-check
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<div
|
||||
v-if="configOptions.length === 0"
|
||||
class="text-center text-body-2 text-medium-emphasis"
|
||||
>
|
||||
暂无可选配置,请先在配置页创建。
|
||||
</div>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="closeDialog"
|
||||
>
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
:disabled="!tempSelectedConfig"
|
||||
:loading="saving"
|
||||
@click="confirmSelection"
|
||||
>
|
||||
应用
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,301 +1,456 @@
|
||||
<template>
|
||||
<div class="sidebar-panel"
|
||||
:class="{
|
||||
'sidebar-collapsed': sidebarCollapsed && !isMobile,
|
||||
'mobile-sidebar-open': isMobile && mobileMenuOpen,
|
||||
'mobile-sidebar': isMobile
|
||||
}"
|
||||
:style="{ backgroundColor: sidebarCollapsed && !isMobile ? 'rgb(var(--v-theme-surface))' : 'rgb(var(--v-theme-mcpCardBg))' }">
|
||||
<div
|
||||
class="sidebar-panel"
|
||||
:class="{
|
||||
'sidebar-collapsed': sidebarCollapsed && !isMobile,
|
||||
'mobile-sidebar-open': isMobile && mobileMenuOpen,
|
||||
'mobile-sidebar': isMobile
|
||||
}"
|
||||
:style="{ backgroundColor: sidebarCollapsed && !isMobile ? 'rgb(var(--v-theme-surface))' : 'rgb(var(--v-theme-mcpCardBg))' }"
|
||||
>
|
||||
<div
|
||||
v-if="!isMobile"
|
||||
class="sidebar-collapse-btn-container"
|
||||
>
|
||||
<v-btn
|
||||
icon
|
||||
class="sidebar-collapse-btn"
|
||||
variant="text"
|
||||
color="deep-purple"
|
||||
@click="toggleSidebar"
|
||||
>
|
||||
<v-icon>{{ sidebarCollapsed ? 'mdi-chevron-right' : 'mdi-chevron-left' }}</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-collapse-btn-container" v-if="!isMobile">
|
||||
<v-btn icon class="sidebar-collapse-btn" @click="toggleSidebar" variant="text" color="deep-purple">
|
||||
<v-icon>{{ sidebarCollapsed ? 'mdi-chevron-right' : 'mdi-chevron-left' }}</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<div
|
||||
v-if="isMobile"
|
||||
class="sidebar-collapse-btn-container"
|
||||
>
|
||||
<v-btn
|
||||
icon
|
||||
class="sidebar-collapse-btn"
|
||||
variant="text"
|
||||
color="deep-purple"
|
||||
@click="$emit('closeMobileSidebar')"
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-collapse-btn-container" v-if="isMobile">
|
||||
<v-btn icon class="sidebar-collapse-btn" @click="$emit('closeMobileSidebar')" variant="text"
|
||||
color="deep-purple">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<div style="padding: 8px; opacity: 0.6;">
|
||||
<div
|
||||
v-if="!sidebarCollapsed || isMobile"
|
||||
class="new-chat-row"
|
||||
>
|
||||
<v-btn
|
||||
block
|
||||
variant="text"
|
||||
class="new-chat-btn"
|
||||
:disabled="!currSessionId && !selectedProjectId"
|
||||
prepend-icon="mdi-square-edit-outline"
|
||||
@click="$emit('newChat')"
|
||||
>
|
||||
{{ tm('actions.newChat') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="sessions.length > 0"
|
||||
icon
|
||||
size="small"
|
||||
variant="text"
|
||||
:color="batchMode ? 'primary' : undefined"
|
||||
@click="toggleBatchMode"
|
||||
>
|
||||
<v-icon>mdi-checkbox-multiple-marked-outline</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-btn
|
||||
v-if="sidebarCollapsed && !isMobile"
|
||||
icon="mdi-square-edit-outline"
|
||||
rounded="xl"
|
||||
:disabled="!currSessionId && !selectedProjectId"
|
||||
elevation="0"
|
||||
@click="$emit('newChat')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style="padding: 8px; opacity: 0.6;">
|
||||
<div class="new-chat-row" v-if="!sidebarCollapsed || isMobile">
|
||||
<v-btn block variant="text" class="new-chat-btn" @click="$emit('newChat')" :disabled="!currSessionId && !selectedProjectId"
|
||||
prepend-icon="mdi-square-edit-outline">{{ tm('actions.newChat') }}</v-btn>
|
||||
<v-btn v-if="sessions.length > 0" icon size="small" variant="text" @click="toggleBatchMode"
|
||||
:color="batchMode ? 'primary' : undefined">
|
||||
<v-icon>mdi-checkbox-multiple-marked-outline</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-btn icon="mdi-square-edit-outline" rounded="xl" @click="$emit('newChat')" :disabled="!currSessionId && !selectedProjectId"
|
||||
v-if="sidebarCollapsed && !isMobile" elevation="0"></v-btn>
|
||||
</div>
|
||||
<!-- Batch action bar -->
|
||||
<div
|
||||
v-if="batchMode && (!sidebarCollapsed || isMobile)"
|
||||
class="batch-action-bar"
|
||||
>
|
||||
<v-btn
|
||||
size="x-small"
|
||||
variant="text"
|
||||
@click="toggleSelectAll"
|
||||
>
|
||||
{{ isAllSelected ? tm('batch.deselectAll') : tm('batch.selectAll') }}
|
||||
</v-btn>
|
||||
<span class="batch-selected-count">{{ tm('batch.selected', { count: batchSelected.length }) }}</span>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
size="x-small"
|
||||
variant="text"
|
||||
color="error"
|
||||
:disabled="batchSelected.length === 0"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
{{ tm('batch.delete') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- Batch action bar -->
|
||||
<div v-if="batchMode && (!sidebarCollapsed || isMobile)" class="batch-action-bar">
|
||||
<v-btn size="x-small" variant="text" @click="toggleSelectAll">
|
||||
{{ isAllSelected ? tm('batch.deselectAll') : tm('batch.selectAll') }}
|
||||
</v-btn>
|
||||
<span class="batch-selected-count">{{ tm('batch.selected', { count: batchSelected.length }) }}</span>
|
||||
<v-spacer />
|
||||
<v-btn size="x-small" variant="text" color="error" :disabled="batchSelected.length === 0"
|
||||
@click="handleBatchDelete">
|
||||
{{ tm('batch.delete') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
<!-- 项目列表组件 -->
|
||||
<ProjectList
|
||||
v-if="!sidebarCollapsed || isMobile"
|
||||
:projects="projects"
|
||||
@selectProject="$emit('selectProject', $event)"
|
||||
@createProject="$emit('createProject')"
|
||||
@editProject="$emit('editProject', $event)"
|
||||
@deleteProject="$emit('deleteProject', $event)"
|
||||
/>
|
||||
|
||||
<!-- 项目列表组件 -->
|
||||
<ProjectList
|
||||
v-if="!sidebarCollapsed || isMobile"
|
||||
:projects="projects"
|
||||
@selectProject="$emit('selectProject', $event)"
|
||||
@createProject="$emit('createProject')"
|
||||
@editProject="$emit('editProject', $event)"
|
||||
@deleteProject="$emit('deleteProject', $event)"
|
||||
/>
|
||||
<div
|
||||
v-if="!sidebarCollapsed || isMobile"
|
||||
style="overflow-y: auto; flex-grow: 1; overscroll-behavior-y: contain;"
|
||||
>
|
||||
<v-card
|
||||
v-if="sessions.length > 0"
|
||||
flat
|
||||
style="background-color: transparent;"
|
||||
>
|
||||
<v-list
|
||||
density="compact"
|
||||
nav
|
||||
class="conversation-list"
|
||||
style="background-color: transparent;"
|
||||
:selected="batchMode ? [] : selectedSessions"
|
||||
@update:selected="handleListSelect"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="item in sessions"
|
||||
:key="item.session_id"
|
||||
:value="item.session_id"
|
||||
rounded="lg"
|
||||
class="conversation-item"
|
||||
active-color="secondary"
|
||||
@click="batchMode ? toggleBatchItem(item.session_id) : undefined"
|
||||
>
|
||||
<template #prepend>
|
||||
<div
|
||||
class="batch-checkbox-slot"
|
||||
:class="{ 'batch-checkbox-slot--active': batchMode }"
|
||||
>
|
||||
<v-checkbox-btn
|
||||
:model-value="batchSelected.includes(item.session_id)"
|
||||
density="compact"
|
||||
hide-details
|
||||
class="batch-checkbox"
|
||||
@update:model-value="toggleBatchItem(item.session_id)"
|
||||
@click.stop
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div style="overflow-y: auto; flex-grow: 1; overscroll-behavior-y: contain;"
|
||||
v-if="!sidebarCollapsed || isMobile">
|
||||
<v-card v-if="sessions.length > 0" flat style="background-color: transparent;">
|
||||
<v-list density="compact" nav class="conversation-list"
|
||||
style="background-color: transparent;" :selected="batchMode ? [] : selectedSessions"
|
||||
@update:selected="handleListSelect">
|
||||
<v-list-item v-for="item in sessions" :key="item.session_id" :value="item.session_id"
|
||||
rounded="lg" class="conversation-item" active-color="secondary"
|
||||
@click="batchMode ? toggleBatchItem(item.session_id) : undefined">
|
||||
|
||||
<template v-slot:prepend>
|
||||
<div class="batch-checkbox-slot" :class="{ 'batch-checkbox-slot--active': batchMode }">
|
||||
<v-checkbox-btn
|
||||
:model-value="batchSelected.includes(item.session_id)"
|
||||
@update:model-value="toggleBatchItem(item.session_id)"
|
||||
@click.stop
|
||||
density="compact"
|
||||
hide-details
|
||||
class="batch-checkbox"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-list-item-title v-if="!sidebarCollapsed || isMobile" class="conversation-title"
|
||||
:style="{ color: 'rgb(var(--v-theme-primaryText))' }">
|
||||
{{ item.display_name || tm('conversation.newConversation') }}
|
||||
</v-list-item-title>
|
||||
<!-- <v-list-item-subtitle v-if="!sidebarCollapsed || isMobile" class="timestamp">
|
||||
<v-list-item-title
|
||||
v-if="!sidebarCollapsed || isMobile"
|
||||
class="conversation-title"
|
||||
:style="{ color: 'rgb(var(--v-theme-primaryText))' }"
|
||||
>
|
||||
{{ item.display_name || tm('conversation.newConversation') }}
|
||||
</v-list-item-title>
|
||||
<!-- <v-list-item-subtitle v-if="!sidebarCollapsed || isMobile" class="timestamp">
|
||||
{{ new Date(item.updated_at).toLocaleString() }}
|
||||
</v-list-item-subtitle> -->
|
||||
|
||||
<template v-if="!batchMode && (!sidebarCollapsed || isMobile)" v-slot:append>
|
||||
<div class="conversation-actions">
|
||||
<v-btn icon="mdi-pencil" size="x-small" variant="text"
|
||||
class="edit-title-btn"
|
||||
@click.stop="$emit('editTitle', item.session_id, item.display_name ?? '')" />
|
||||
<v-btn icon="mdi-delete" size="x-small" variant="text"
|
||||
class="delete-conversation-btn" color="error"
|
||||
@click.stop="handleDeleteConversation(item)" />
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
<template
|
||||
v-if="!batchMode && (!sidebarCollapsed || isMobile)"
|
||||
#append
|
||||
>
|
||||
<div class="conversation-actions">
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
size="x-small"
|
||||
variant="text"
|
||||
class="edit-title-btn"
|
||||
@click.stop="$emit('editTitle', item.session_id, item.display_name ?? '')"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
size="x-small"
|
||||
variant="text"
|
||||
class="delete-conversation-btn"
|
||||
color="error"
|
||||
@click.stop="handleDeleteConversation(item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
|
||||
<v-fade-transition>
|
||||
<div class="no-conversations" v-if="sessions.length === 0">
|
||||
<v-icon icon="mdi-message-text-outline" size="large" color="grey-lighten-1"></v-icon>
|
||||
<div class="no-conversations-text" v-if="!sidebarCollapsed || isMobile">
|
||||
{{ tm('conversation.noHistory') }}
|
||||
</div>
|
||||
</div>
|
||||
</v-fade-transition>
|
||||
<v-fade-transition>
|
||||
<div
|
||||
v-if="sessions.length === 0"
|
||||
class="no-conversations"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-message-text-outline"
|
||||
size="large"
|
||||
color="grey-lighten-1"
|
||||
/>
|
||||
<div
|
||||
v-if="!sidebarCollapsed || isMobile"
|
||||
class="no-conversations-text"
|
||||
>
|
||||
{{ tm('conversation.noHistory') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 收起时的占位元素 -->
|
||||
<div class="sidebar-spacer" v-if="sidebarCollapsed && !isMobile"></div>
|
||||
|
||||
<!-- 底部设置按钮 -->
|
||||
<div class="sidebar-footer">
|
||||
<StyledMenu location="top" :close-on-content-click="false">
|
||||
<template v-slot:activator="{ props: menuProps }">
|
||||
<v-btn
|
||||
v-bind="menuProps"
|
||||
:icon="sidebarCollapsed && !isMobile"
|
||||
:block="!sidebarCollapsed || isMobile"
|
||||
variant="text"
|
||||
class="settings-btn"
|
||||
:class="{ 'settings-btn-collapsed': sidebarCollapsed && !isMobile }"
|
||||
:prepend-icon="(!sidebarCollapsed || isMobile) ? 'mdi-cog-outline' : undefined"
|
||||
>
|
||||
<v-icon v-if="sidebarCollapsed && !isMobile">mdi-cog-outline</v-icon>
|
||||
<template v-if="!sidebarCollapsed || isMobile">{{ t('core.common.settings') }}</template>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<!-- 语言切换(分组) -->
|
||||
<v-menu
|
||||
:open-on-hover="!isMobile"
|
||||
:open-on-click="isMobile"
|
||||
:open-delay="!isMobile ? 60 : 0"
|
||||
:close-delay="!isMobile ? 120 : 0"
|
||||
:location="isMobile ? 'bottom' : 'end center'"
|
||||
offset="8"
|
||||
close-on-content-click
|
||||
>
|
||||
<template v-slot:activator="{ props: languageMenuProps }">
|
||||
<v-list-item
|
||||
v-bind="languageMenuProps"
|
||||
class="styled-menu-item chat-settings-group-trigger"
|
||||
rounded="md"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-translate</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ t('core.common.language') }}</v-list-item-title>
|
||||
<template v-slot:append>
|
||||
<span class="chat-settings-group-current">{{ currentLanguage?.flag }}</span>
|
||||
<v-icon size="18" class="chat-settings-group-arrow">mdi-chevron-right</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<v-card class="styled-menu-card" style="min-width: 180px;" elevation="8" rounded="lg">
|
||||
<v-list density="compact" class="styled-menu-list pa-1">
|
||||
<v-list-item
|
||||
v-for="lang in languages"
|
||||
:key="lang.code"
|
||||
:value="lang.code"
|
||||
@click="changeLanguage(lang.code)"
|
||||
:class="{ 'styled-menu-item-active': currentLocale === lang.code }"
|
||||
class="styled-menu-item"
|
||||
rounded="md"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<span class="language-flag">{{ lang.flag }}</span>
|
||||
</template>
|
||||
<v-list-item-title>{{ lang.name }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
|
||||
<!-- 主题切换 -->
|
||||
<v-list-item class="styled-menu-item" @click="$emit('toggleTheme')">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>{{ isDark ? 'mdi-weather-night' : 'mdi-white-balance-sunny' }}</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ isDark ? tm('modes.lightMode') : tm('modes.darkMode') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<!-- 通信传输模式(分组) -->
|
||||
<v-menu
|
||||
:open-on-hover="!isMobile"
|
||||
:open-on-click="isMobile"
|
||||
:open-delay="!isMobile ? 60 : 0"
|
||||
:close-delay="!isMobile ? 120 : 0"
|
||||
:location="isMobile ? 'bottom' : 'end center'"
|
||||
offset="8"
|
||||
close-on-content-click
|
||||
>
|
||||
<template v-slot:activator="{ props: transportMenuProps }">
|
||||
<v-list-item
|
||||
v-bind="transportMenuProps"
|
||||
class="styled-menu-item chat-settings-group-trigger"
|
||||
rounded="md"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-lan-connect</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ tm('transport.title') }}</v-list-item-title>
|
||||
<template v-slot:append>
|
||||
<span class="chat-settings-group-current chat-settings-transport-current">{{ currentTransportLabel }}</span>
|
||||
<v-icon size="18" class="chat-settings-group-arrow">mdi-chevron-right</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<v-card class="styled-menu-card" style="min-width: 220px;" elevation="8" rounded="lg">
|
||||
<v-list density="compact" class="styled-menu-list pa-1">
|
||||
<v-list-item
|
||||
v-for="opt in transportOptions"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
@click="handleTransportModeChange(opt.value)"
|
||||
:class="{ 'styled-menu-item-active': transportMode === opt.value }"
|
||||
class="styled-menu-item"
|
||||
rounded="md"
|
||||
>
|
||||
<v-list-item-title>{{ opt.label }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
|
||||
<!-- 发送快捷键(分组) -->
|
||||
<v-menu
|
||||
:open-on-hover="!isMobile"
|
||||
:open-on-click="isMobile"
|
||||
:open-delay="!isMobile ? 60 : 0"
|
||||
:close-delay="!isMobile ? 120 : 0"
|
||||
:location="isMobile ? 'bottom' : 'end center'"
|
||||
offset="8"
|
||||
close-on-content-click
|
||||
>
|
||||
<template v-slot:activator="{ props: sendShortcutMenuProps }">
|
||||
<v-list-item
|
||||
v-bind="sendShortcutMenuProps"
|
||||
class="styled-menu-item chat-settings-group-trigger"
|
||||
rounded="md"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-keyboard-outline</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ tm('shortcuts.sendKey.title') }}</v-list-item-title>
|
||||
<template v-slot:append>
|
||||
<span class="chat-settings-group-current chat-settings-transport-current">{{ currentSendShortcutLabel }}</span>
|
||||
<v-icon size="18" class="chat-settings-group-arrow">mdi-chevron-right</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<v-card class="styled-menu-card" style="min-width: 220px;" elevation="8" rounded="lg">
|
||||
<v-list density="compact" class="styled-menu-list pa-1">
|
||||
<v-list-item
|
||||
v-for="opt in sendShortcutOptions"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
@click="handleSendShortcutChange(opt.value)"
|
||||
:class="{ 'styled-menu-item-active': props.sendShortcut === opt.value }"
|
||||
class="styled-menu-item"
|
||||
rounded="md"
|
||||
>
|
||||
<v-list-item-title>{{ opt.label }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
|
||||
<!-- 全屏/退出全屏 -->
|
||||
<v-list-item class="styled-menu-item" @click="$emit('toggleFullscreen')">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>{{ chatboxMode ? 'mdi-fullscreen-exit' : 'mdi-fullscreen' }}</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ chatboxMode ? tm('actions.exitFullscreen') : tm('actions.fullscreen') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<!-- 提供商配置 -->
|
||||
<v-list-item class="styled-menu-item" @click="showProviderConfigDialog = true">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-creation</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ tm('actions.providerConfig') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</StyledMenu>
|
||||
</div>
|
||||
|
||||
<!-- 提供商配置对话框 -->
|
||||
<ProviderConfigDialog v-model="showProviderConfigDialog" />
|
||||
</v-fade-transition>
|
||||
</div>
|
||||
|
||||
<!-- 收起时的占位元素 -->
|
||||
<div
|
||||
v-if="sidebarCollapsed && !isMobile"
|
||||
class="sidebar-spacer"
|
||||
/>
|
||||
|
||||
<!-- 底部设置按钮 -->
|
||||
<div class="sidebar-footer">
|
||||
<StyledMenu
|
||||
location="top"
|
||||
:close-on-content-click="false"
|
||||
>
|
||||
<template #activator="{ props: menuProps }">
|
||||
<v-btn
|
||||
v-bind="menuProps"
|
||||
:icon="sidebarCollapsed && !isMobile"
|
||||
:block="!sidebarCollapsed || isMobile"
|
||||
variant="text"
|
||||
class="settings-btn"
|
||||
:class="{ 'settings-btn-collapsed': sidebarCollapsed && !isMobile }"
|
||||
:prepend-icon="(!sidebarCollapsed || isMobile) ? 'mdi-cog-outline' : undefined"
|
||||
>
|
||||
<v-icon v-if="sidebarCollapsed && !isMobile">
|
||||
mdi-cog-outline
|
||||
</v-icon>
|
||||
<template v-if="!sidebarCollapsed || isMobile">
|
||||
{{ t('core.common.settings') }}
|
||||
</template>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<!-- 语言切换(分组) -->
|
||||
<v-menu
|
||||
:open-on-hover="!isMobile"
|
||||
:open-on-click="isMobile"
|
||||
:open-delay="!isMobile ? 60 : 0"
|
||||
:close-delay="!isMobile ? 120 : 0"
|
||||
:location="isMobile ? 'bottom' : 'end center'"
|
||||
offset="8"
|
||||
close-on-content-click
|
||||
>
|
||||
<template #activator="{ props: languageMenuProps }">
|
||||
<v-list-item
|
||||
v-bind="languageMenuProps"
|
||||
class="styled-menu-item chat-settings-group-trigger"
|
||||
rounded="md"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-translate</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ t('core.common.language') }}</v-list-item-title>
|
||||
<template #append>
|
||||
<span class="chat-settings-group-current">{{ currentLanguage?.flag }}</span>
|
||||
<v-icon
|
||||
size="18"
|
||||
class="chat-settings-group-arrow"
|
||||
>
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<v-card
|
||||
class="styled-menu-card"
|
||||
style="min-width: 180px;"
|
||||
elevation="8"
|
||||
rounded="lg"
|
||||
>
|
||||
<v-list
|
||||
density="compact"
|
||||
class="styled-menu-list pa-1"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="lang in languages"
|
||||
:key="lang.code"
|
||||
:value="lang.code"
|
||||
:class="{ 'styled-menu-item-active': currentLocale === lang.code }"
|
||||
class="styled-menu-item"
|
||||
rounded="md"
|
||||
@click="changeLanguage(lang.code)"
|
||||
>
|
||||
<template #prepend>
|
||||
<span class="language-flag">{{ lang.flag }}</span>
|
||||
</template>
|
||||
<v-list-item-title>{{ lang.name }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
|
||||
<!-- 主题切换 -->
|
||||
<v-list-item
|
||||
class="styled-menu-item"
|
||||
@click="$emit('toggleTheme')"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon>{{ isDark ? 'mdi-weather-night' : 'mdi-white-balance-sunny' }}</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ isDark ? tm('modes.lightMode') : tm('modes.darkMode') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<!-- 通信传输模式(分组) -->
|
||||
<v-menu
|
||||
:open-on-hover="!isMobile"
|
||||
:open-on-click="isMobile"
|
||||
:open-delay="!isMobile ? 60 : 0"
|
||||
:close-delay="!isMobile ? 120 : 0"
|
||||
:location="isMobile ? 'bottom' : 'end center'"
|
||||
offset="8"
|
||||
close-on-content-click
|
||||
>
|
||||
<template #activator="{ props: transportMenuProps }">
|
||||
<v-list-item
|
||||
v-bind="transportMenuProps"
|
||||
class="styled-menu-item chat-settings-group-trigger"
|
||||
rounded="md"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-lan-connect</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ tm('transport.title') }}</v-list-item-title>
|
||||
<template #append>
|
||||
<span class="chat-settings-group-current chat-settings-transport-current">{{ currentTransportLabel }}</span>
|
||||
<v-icon
|
||||
size="18"
|
||||
class="chat-settings-group-arrow"
|
||||
>
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<v-card
|
||||
class="styled-menu-card"
|
||||
style="min-width: 220px;"
|
||||
elevation="8"
|
||||
rounded="lg"
|
||||
>
|
||||
<v-list
|
||||
density="compact"
|
||||
class="styled-menu-list pa-1"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="opt in transportOptions"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
:class="{ 'styled-menu-item-active': transportMode === opt.value }"
|
||||
class="styled-menu-item"
|
||||
rounded="md"
|
||||
@click="handleTransportModeChange(opt.value)"
|
||||
>
|
||||
<v-list-item-title>{{ opt.label }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
|
||||
<!-- 发送快捷键(分组) -->
|
||||
<v-menu
|
||||
:open-on-hover="!isMobile"
|
||||
:open-on-click="isMobile"
|
||||
:open-delay="!isMobile ? 60 : 0"
|
||||
:close-delay="!isMobile ? 120 : 0"
|
||||
:location="isMobile ? 'bottom' : 'end center'"
|
||||
offset="8"
|
||||
close-on-content-click
|
||||
>
|
||||
<template #activator="{ props: sendShortcutMenuProps }">
|
||||
<v-list-item
|
||||
v-bind="sendShortcutMenuProps"
|
||||
class="styled-menu-item chat-settings-group-trigger"
|
||||
rounded="md"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-keyboard-outline</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ tm('shortcuts.sendKey.title') }}</v-list-item-title>
|
||||
<template #append>
|
||||
<span class="chat-settings-group-current chat-settings-transport-current">{{ currentSendShortcutLabel }}</span>
|
||||
<v-icon
|
||||
size="18"
|
||||
class="chat-settings-group-arrow"
|
||||
>
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<v-card
|
||||
class="styled-menu-card"
|
||||
style="min-width: 220px;"
|
||||
elevation="8"
|
||||
rounded="lg"
|
||||
>
|
||||
<v-list
|
||||
density="compact"
|
||||
class="styled-menu-list pa-1"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="opt in sendShortcutOptions"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
:class="{ 'styled-menu-item-active': props.sendShortcut === opt.value }"
|
||||
class="styled-menu-item"
|
||||
rounded="md"
|
||||
@click="handleSendShortcutChange(opt.value)"
|
||||
>
|
||||
<v-list-item-title>{{ opt.label }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
|
||||
<!-- 全屏/退出全屏 -->
|
||||
<v-list-item
|
||||
class="styled-menu-item"
|
||||
@click="$emit('toggleFullscreen')"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon>{{ chatboxMode ? 'mdi-fullscreen-exit' : 'mdi-fullscreen' }}</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ chatboxMode ? tm('actions.exitFullscreen') : tm('actions.fullscreen') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<!-- 提供商配置 -->
|
||||
<v-list-item
|
||||
class="styled-menu-item"
|
||||
@click="showProviderConfigDialog = true"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-creation</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ tm('actions.providerConfig') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</StyledMenu>
|
||||
</div>
|
||||
|
||||
<!-- 提供商配置对话框 -->
|
||||
<ProviderConfigDialog v-model="showProviderConfigDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<template>
|
||||
<div class="live-mode-container">
|
||||
<div class="header-controls">
|
||||
<v-btn icon="mdi-close" @click="handleClose" flat variant="text" />
|
||||
<v-btn
|
||||
icon="mdi-close"
|
||||
flat
|
||||
variant="text"
|
||||
@click="handleClose"
|
||||
/>
|
||||
<v-btn
|
||||
:icon="isCodeMode ? 'mdi-code-tags-check' : 'mdi-code-tags'"
|
||||
@click="toggleCodeMode"
|
||||
flat
|
||||
variant="text"
|
||||
:color="isCodeMode ? 'primary' : ''"
|
||||
@click="toggleCodeMode"
|
||||
/>
|
||||
<v-btn
|
||||
:icon="
|
||||
@@ -15,22 +20,26 @@
|
||||
? 'mdi-emoticon-confused'
|
||||
: 'mdi-emoticon-confused-outline'
|
||||
"
|
||||
@click="toggleNervousMode"
|
||||
flat
|
||||
variant="text"
|
||||
:color="isNervousMode ? 'primary' : ''"
|
||||
@click="toggleNervousMode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span style="color: gray; padding-left: 16px"
|
||||
>We're developing Astr Live Mode on ChatUI & Desktop right now. Stay
|
||||
tuned!</span
|
||||
>
|
||||
<span style="color: gray; padding-left: 16px">We're developing Astr Live Mode on ChatUI & Desktop right now. Stay
|
||||
tuned!</span>
|
||||
|
||||
<div class="live-mode-content">
|
||||
<div class="center-circle-container" @click="handleCircleClick">
|
||||
<div
|
||||
class="center-circle-container"
|
||||
@click="handleCircleClick"
|
||||
>
|
||||
<!-- 爆炸效果层 -->
|
||||
<div v-if="isExploding" class="explosion-wave"></div>
|
||||
<div
|
||||
v-if="isExploding"
|
||||
class="explosion-wave"
|
||||
/>
|
||||
|
||||
<SiriOrb
|
||||
:energy="orbEnergy"
|
||||
@@ -44,7 +53,10 @@
|
||||
<div class="status-text">
|
||||
{{ statusText }}
|
||||
</div>
|
||||
<div class="messages-container" v-if="messages.length > 0">
|
||||
<div
|
||||
v-if="messages.length > 0"
|
||||
class="messages-container"
|
||||
>
|
||||
<div
|
||||
v-for="(msg, index) in messages"
|
||||
:key="index"
|
||||
@@ -57,40 +69,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metrics-container" v-if="Object.keys(metrics).length > 0">
|
||||
<span v-if="metrics.wav_assemble_time"
|
||||
>WAV Assemble:
|
||||
{{ (metrics.wav_assemble_time * 1000).toFixed(0) }}ms</span
|
||||
>
|
||||
<span v-if="metrics.llm_ttft"
|
||||
>LLM First Token Latency:
|
||||
{{ (metrics.llm_ttft * 1000).toFixed(0) }}ms</span
|
||||
>
|
||||
<span v-if="metrics.llm_total_time"
|
||||
>LLM Total Latency:
|
||||
{{ (metrics.llm_total_time * 1000).toFixed(0) }}ms</span
|
||||
>
|
||||
<span v-if="metrics.tts_first_frame_time"
|
||||
>TTS First Frame Latency:
|
||||
{{ (metrics.tts_first_frame_time * 1000).toFixed(0) }}ms</span
|
||||
>
|
||||
<span v-if="metrics.tts_total_time"
|
||||
>TTS Total Larency:
|
||||
{{ (metrics.tts_total_time * 1000).toFixed(0) }}ms</span
|
||||
>
|
||||
<span v-if="metrics.speak_to_first_frame"
|
||||
>Speak -> First TTS Frame:
|
||||
{{ (metrics.speak_to_first_frame * 1000).toFixed(0) }}ms</span
|
||||
>
|
||||
<span v-if="metrics.wav_to_tts_total_time"
|
||||
>Speak -> End:
|
||||
{{ (metrics.wav_to_tts_total_time * 1000).toFixed(0) }}ms</span
|
||||
>
|
||||
<div
|
||||
v-if="Object.keys(metrics).length > 0"
|
||||
class="metrics-container"
|
||||
>
|
||||
<span v-if="metrics.wav_assemble_time">WAV Assemble:
|
||||
{{ (metrics.wav_assemble_time * 1000).toFixed(0) }}ms</span>
|
||||
<span v-if="metrics.llm_ttft">LLM First Token Latency:
|
||||
{{ (metrics.llm_ttft * 1000).toFixed(0) }}ms</span>
|
||||
<span v-if="metrics.llm_total_time">LLM Total Latency:
|
||||
{{ (metrics.llm_total_time * 1000).toFixed(0) }}ms</span>
|
||||
<span v-if="metrics.tts_first_frame_time">TTS First Frame Latency:
|
||||
{{ (metrics.tts_first_frame_time * 1000).toFixed(0) }}ms</span>
|
||||
<span v-if="metrics.tts_total_time">TTS Total Larency:
|
||||
{{ (metrics.tts_total_time * 1000).toFixed(0) }}ms</span>
|
||||
<span v-if="metrics.speak_to_first_frame">Speak -> First TTS Frame:
|
||||
{{ (metrics.speak_to_first_frame * 1000).toFixed(0) }}ms</span>
|
||||
<span v-if="metrics.wav_to_tts_total_time">Speak -> End:
|
||||
{{ (metrics.wav_to_tts_total_time * 1000).toFixed(0) }}ms</span>
|
||||
<span v-if="metrics.stt">STT Provider: {{ metrics.stt }}</span>
|
||||
<span v-if="metrics.tts">TTS Provider: {{ metrics.tts }}</span>
|
||||
<span v-if="metrics.chat_model"
|
||||
>Chat Model: {{ metrics.chat_model }}</span
|
||||
>
|
||||
<span v-if="metrics.chat_model">Chat Model: {{ metrics.chat_model }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,53 +1,111 @@
|
||||
<template>
|
||||
<div class="live-orb-container" ref="containerRef" :class="{ 'dark': isDark }" :style="styleVars">
|
||||
<div class="live-orb">
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="live-orb-container"
|
||||
:class="{ 'dark': isDark }"
|
||||
:style="styleVars"
|
||||
>
|
||||
<div class="live-orb" />
|
||||
<div class="eyes-container">
|
||||
<div
|
||||
class="eye"
|
||||
:class="{ 'blink': isBlinking, 'nervous': nervousMode }"
|
||||
>
|
||||
<!-- Nervous Mode > -->
|
||||
<div
|
||||
v-if="nervousMode"
|
||||
class="nervous-eye-content"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 30 60"
|
||||
width="100%"
|
||||
height="100%"
|
||||
>
|
||||
<path
|
||||
d="M 0 10 L 30 30 L 0 50"
|
||||
fill="none"
|
||||
stroke="#7d80e4"
|
||||
stroke-width="8"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="eyes-container">
|
||||
<div class="eye" :class="{ 'blink': isBlinking, 'nervous': nervousMode }">
|
||||
<!-- Nervous Mode > -->
|
||||
<div v-if="nervousMode" class="nervous-eye-content">
|
||||
<svg viewBox="0 0 30 60" width="100%" height="100%">
|
||||
<path d="M 0 10 L 30 30 L 0 50" fill="none" stroke="#7d80e4" stroke-width="8" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Code Mode Layer -->
|
||||
<transition name="fade">
|
||||
<div v-if="codeMode && !nervousMode" class="code-rain-container">
|
||||
<div v-for="(col, i) in codeColumns" :key="i" class="code-column" :style="col.style">
|
||||
{{ col.content }}
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<!-- Code Mode Layer -->
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="codeMode && !nervousMode"
|
||||
class="code-rain-container"
|
||||
>
|
||||
<div
|
||||
v-for="(col, i) in codeColumns"
|
||||
:key="i"
|
||||
class="code-column"
|
||||
:style="col.style"
|
||||
>
|
||||
{{ col.content }}
|
||||
</div>
|
||||
<div class="eye" :class="{ 'blink': isBlinking, 'nervous': nervousMode }">
|
||||
<!-- Nervous Mode < -->
|
||||
<div v-if="nervousMode" class="nervous-eye-content">
|
||||
<svg viewBox="0 0 30 60" width="100%" height="100%">
|
||||
<path d="M 30 10 L 0 30 L 30 50" fill="none" stroke="#7d80e4" stroke-width="8" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
<div
|
||||
class="eye"
|
||||
:class="{ 'blink': isBlinking, 'nervous': nervousMode }"
|
||||
>
|
||||
<!-- Nervous Mode < -->
|
||||
<div
|
||||
v-if="nervousMode"
|
||||
class="nervous-eye-content"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 30 60"
|
||||
width="100%"
|
||||
height="100%"
|
||||
>
|
||||
<path
|
||||
d="M 30 10 L 0 30 L 30 50"
|
||||
fill="none"
|
||||
stroke="#7d80e4"
|
||||
stroke-width="8"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Code Mode Layer -->
|
||||
<transition name="fade">
|
||||
<div v-if="codeMode && !nervousMode" class="code-rain-container">
|
||||
<div v-for="(col, i) in codeColumns" :key="i" class="code-column" :style="col.style">
|
||||
{{ col.content }}
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<!-- Code Mode Layer -->
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="codeMode && !nervousMode"
|
||||
class="code-rain-container"
|
||||
>
|
||||
<div
|
||||
v-for="(col, i) in codeColumns"
|
||||
:key="i"
|
||||
class="code-column"
|
||||
:style="col.style"
|
||||
>
|
||||
{{ col.content }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hair Accessory Star -->
|
||||
<div class="accessory-star">
|
||||
<svg viewBox="0 0 24 24" width="100%" height="100%">
|
||||
<path d="M12 2l2.4 7.2h7.6l-6 4.8 2.4 7.2-6-4.8-6 4.8 2.4-7.2-6-4.8h7.6z"
|
||||
fill="rgba(125, 128, 228, 0.4)" stroke="rgba(180, 182, 255, 0.6)" stroke-width="3"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hair Accessory Star -->
|
||||
<div class="accessory-star">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
height="100%"
|
||||
>
|
||||
<path
|
||||
d="M12 2l2.4 7.2h7.6l-6 4.8 2.4 7.2-6-4.8-6 4.8 2.4-7.2-6-4.8h7.6z"
|
||||
fill="rgba(125, 128, 228, 0.4)"
|
||||
stroke="rgba(180, 182, 255, 0.6)"
|
||||
stroke-width="3"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,62 @@
|
||||
<template>
|
||||
<v-dialog v-model="isOpen" max-width="500" @update:model-value="handleDialogChange">
|
||||
<v-card>
|
||||
<v-card-title class="dialog-title">
|
||||
{{ isEditing ? tm('project.edit') : tm('project.create') }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field v-model="form.emoji" :label="tm('project.emoji')" flat variant="solo-filled" hide-details class="mb-3" />
|
||||
<v-text-field v-model="form.title" :label="tm('project.name')" flat variant="solo-filled" hide-details class="mb-3" autofocus
|
||||
@keyup.enter="handleSave" />
|
||||
<v-textarea v-model="form.description" :label="tm('project.description')" flat variant="solo-filled" hide-details rows="3" rounded="lg" />
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="handleCancel" color="grey-darken-1">{{ t('core.common.cancel') }}</v-btn>
|
||||
<v-btn variant="text" @click="handleSave" color="primary" :disabled="!form.title.trim()">{{ t('core.common.save') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-dialog
|
||||
v-model="isOpen"
|
||||
max-width="500"
|
||||
@update:model-value="handleDialogChange"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="dialog-title">
|
||||
{{ isEditing ? tm('project.edit') : tm('project.create') }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="form.emoji"
|
||||
:label="tm('project.emoji')"
|
||||
flat
|
||||
variant="solo-filled"
|
||||
hide-details
|
||||
class="mb-3"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="form.title"
|
||||
:label="tm('project.name')"
|
||||
flat
|
||||
variant="solo-filled"
|
||||
hide-details
|
||||
class="mb-3"
|
||||
autofocus
|
||||
@keyup.enter="handleSave"
|
||||
/>
|
||||
<v-textarea
|
||||
v-model="form.description"
|
||||
:label="tm('project.description')"
|
||||
flat
|
||||
variant="solo-filled"
|
||||
hide-details
|
||||
rows="3"
|
||||
rounded="lg"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
color="grey-darken-1"
|
||||
@click="handleCancel"
|
||||
>
|
||||
{{ t('core.common.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="text"
|
||||
color="primary"
|
||||
:disabled="!form.title.trim()"
|
||||
@click="handleSave"
|
||||
>
|
||||
{{ t('core.common.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,44 +1,84 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 项目按钮 -->
|
||||
<div style="padding: 0 8px 0px 8px; opacity: 0.6;">
|
||||
<v-btn block variant="text" class="project-btn" @click="toggleExpanded" prepend-icon="mdi-folder-outline">
|
||||
{{ tm('project.title') }}
|
||||
<template v-slot:append>
|
||||
<v-icon size="small">{{ expanded ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
|
||||
</template>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- 项目列表 -->
|
||||
<v-expand-transition>
|
||||
<div v-show="expanded" style="padding: 0 8px;">
|
||||
<v-list density="compact" nav class="project-list" style="background-color: transparent;">
|
||||
<v-list-item @click="$emit('createProject')" class="create-project-item" rounded="lg">
|
||||
<template v-slot:prepend>
|
||||
<span class="project-emoji"><v-icon size="small">mdi-plus</v-icon></span>
|
||||
</template>
|
||||
<v-list-item-title style="font-size: 13px;">{{ tm('project.create') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item v-for="project in projects" :key="project.project_id"
|
||||
@click="$emit('selectProject', project.project_id)" rounded="lg" class="project-item">
|
||||
<template v-slot:prepend>
|
||||
<span class="project-emoji">{{ project.emoji || '📁' }}</span>
|
||||
</template>
|
||||
<v-list-item-title class="project-title">{{ project.title }}</v-list-item-title>
|
||||
<template v-slot:append>
|
||||
<div class="project-actions">
|
||||
<v-btn icon="mdi-pencil" size="x-small" variant="text" class="edit-project-btn"
|
||||
@click.stop="$emit('editProject', project)" />
|
||||
<v-btn icon="mdi-delete" size="x-small" variant="text" class="delete-project-btn"
|
||||
color="error" @click.stop="handleDeleteProject(project)" />
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
<div>
|
||||
<!-- 项目按钮 -->
|
||||
<div style="padding: 0 8px 0px 8px; opacity: 0.6;">
|
||||
<v-btn
|
||||
block
|
||||
variant="text"
|
||||
class="project-btn"
|
||||
prepend-icon="mdi-folder-outline"
|
||||
@click="toggleExpanded"
|
||||
>
|
||||
{{ tm('project.title') }}
|
||||
<template #append>
|
||||
<v-icon size="small">
|
||||
{{ expanded ? 'mdi-chevron-up' : 'mdi-chevron-down' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- 项目列表 -->
|
||||
<v-expand-transition>
|
||||
<div
|
||||
v-show="expanded"
|
||||
style="padding: 0 8px;"
|
||||
>
|
||||
<v-list
|
||||
density="compact"
|
||||
nav
|
||||
class="project-list"
|
||||
style="background-color: transparent;"
|
||||
>
|
||||
<v-list-item
|
||||
class="create-project-item"
|
||||
rounded="lg"
|
||||
@click="$emit('createProject')"
|
||||
>
|
||||
<template #prepend>
|
||||
<span class="project-emoji"><v-icon size="small">mdi-plus</v-icon></span>
|
||||
</template>
|
||||
<v-list-item-title style="font-size: 13px;">
|
||||
{{ tm('project.create') }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-for="project in projects"
|
||||
:key="project.project_id"
|
||||
rounded="lg"
|
||||
class="project-item"
|
||||
@click="$emit('selectProject', project.project_id)"
|
||||
>
|
||||
<template #prepend>
|
||||
<span class="project-emoji">{{ project.emoji || '📁' }}</span>
|
||||
</template>
|
||||
<v-list-item-title class="project-title">
|
||||
{{ project.title }}
|
||||
</v-list-item-title>
|
||||
<template #append>
|
||||
<div class="project-actions">
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
size="x-small"
|
||||
variant="text"
|
||||
class="edit-project-btn"
|
||||
@click.stop="$emit('editProject', project)"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
size="x-small"
|
||||
variant="text"
|
||||
class="delete-project-btn"
|
||||
color="error"
|
||||
@click.stop="handleDeleteProject(project)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,47 +1,76 @@
|
||||
<template>
|
||||
<div class="project-sessions-container fade-in">
|
||||
<div class="project-header">
|
||||
<div class="project-header-info">
|
||||
<span class="project-header-emoji">{{ project?.emoji || '📁' }}</span>
|
||||
<h2 class="project-header-title">{{ project?.title }}</h2>
|
||||
</div>
|
||||
<p class="project-header-description" v-if="project?.description">
|
||||
{{ project.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="project-input-slot">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<v-card flat class="project-sessions-list">
|
||||
<v-list v-if="sessions.length > 0">
|
||||
<v-list-item v-for="session in sessions" :key="session.session_id"
|
||||
@click="$emit('selectSession', session.session_id)" class="project-session-item" rounded="lg">
|
||||
<v-list-item-title>
|
||||
{{ session.display_name || tm('conversation.newConversation') }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ formatDate(session.updated_at) }}
|
||||
</v-list-item-subtitle>
|
||||
<template v-slot:append>
|
||||
<div class="session-actions">
|
||||
<v-btn icon="mdi-pencil" size="x-small" variant="text"
|
||||
class="edit-session-btn"
|
||||
@click.stop="$emit('editSessionTitle', session.session_id, session.display_name ?? '')" />
|
||||
<v-btn icon="mdi-delete" size="x-small" variant="text"
|
||||
class="delete-session-btn" color="error"
|
||||
@click.stop="handleDeleteSession(session)" />
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<div v-else class="no-sessions-in-project">
|
||||
<v-icon icon="mdi-message-off-outline" size="large" color="grey-lighten-1"></v-icon>
|
||||
<p>{{ tm('project.noSessions') }}</p>
|
||||
</div>
|
||||
</v-card>
|
||||
<div class="project-sessions-container fade-in">
|
||||
<div class="project-header">
|
||||
<div class="project-header-info">
|
||||
<span class="project-header-emoji">{{ project?.emoji || '📁' }}</span>
|
||||
<h2 class="project-header-title">
|
||||
{{ project?.title }}
|
||||
</h2>
|
||||
</div>
|
||||
<p
|
||||
v-if="project?.description"
|
||||
class="project-header-description"
|
||||
>
|
||||
{{ project.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="project-input-slot">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<v-card
|
||||
flat
|
||||
class="project-sessions-list"
|
||||
>
|
||||
<v-list v-if="sessions.length > 0">
|
||||
<v-list-item
|
||||
v-for="session in sessions"
|
||||
:key="session.session_id"
|
||||
class="project-session-item"
|
||||
rounded="lg"
|
||||
@click="$emit('selectSession', session.session_id)"
|
||||
>
|
||||
<v-list-item-title>
|
||||
{{ session.display_name || tm('conversation.newConversation') }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ formatDate(session.updated_at) }}
|
||||
</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<div class="session-actions">
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
size="x-small"
|
||||
variant="text"
|
||||
class="edit-session-btn"
|
||||
@click.stop="$emit('editSessionTitle', session.session_id, session.display_name ?? '')"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
size="x-small"
|
||||
variant="text"
|
||||
class="delete-session-btn"
|
||||
color="error"
|
||||
@click.stop="handleDeleteSession(session)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<div
|
||||
v-else
|
||||
class="no-sessions-in-project"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-message-off-outline"
|
||||
size="large"
|
||||
color="grey-lighten-1"
|
||||
/>
|
||||
<p>{{ tm('project.noSessions') }}</p>
|
||||
</div>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,80 +1,161 @@
|
||||
<template>
|
||||
<v-dialog v-model="dialog" :max-width="isMobile ? undefined : '1400'" :fullscreen="isMobile" scrollable>
|
||||
<v-card class="provider-config-dialog" :class="{ 'mobile-dialog': isMobile }">
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
:max-width="isMobile ? undefined : '1400'"
|
||||
:fullscreen="isMobile"
|
||||
scrollable
|
||||
>
|
||||
<v-card
|
||||
class="provider-config-dialog"
|
||||
:class="{ 'mobile-dialog': isMobile }"
|
||||
>
|
||||
<v-card-title class="d-flex align-center justify-space-between pa-4 pb-0">
|
||||
<div class="d-flex align-center ga-2">
|
||||
<span class="text-h2 font-weight-bold">{{ tm('title') }}</span>
|
||||
</div>
|
||||
<v-btn icon variant="text" @click="closeDialog">
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
@click="closeDialog"
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-4 pt-0" :class="{ 'mobile-content': isMobile }"
|
||||
:style="isMobile ? {} : { height: 'calc(100vh - 200px); max-height: 800px;' }">
|
||||
<div :class="isMobile ? 'mobile-layout' : 'd-flex'" :style="isMobile ? {} : { height: '100%' }">
|
||||
<v-card-text
|
||||
class="pa-4 pt-0"
|
||||
:class="{ 'mobile-content': isMobile }"
|
||||
:style="isMobile ? {} : { height: 'calc(100vh - 200px); max-height: 800px;' }"
|
||||
>
|
||||
<div
|
||||
:class="isMobile ? 'mobile-layout' : 'd-flex'"
|
||||
:style="isMobile ? {} : { height: '100%' }"
|
||||
>
|
||||
<!-- 左侧:Provider Sources 列表 -->
|
||||
<div class="provider-sources-column" :class="{ 'mobile-sources': isMobile }"
|
||||
:style="isMobile ? {} : { width: '320px', minWidth: '320px', borderRight: '1px solid rgba(var(--v-border-color), var(--v-border-opacity))', overflowY: 'auto' }">
|
||||
<ProviderSourcesPanel :displayed-provider-sources="displayedProviderSources"
|
||||
:selected-provider-source="selectedProviderSource" :available-source-types="availableSourceTypes" :tm="tm"
|
||||
:resolve-source-icon="resolveSourceIcon" :get-source-display-name="getSourceDisplayName"
|
||||
@add-provider-source="addProviderSource" @select-provider-source="selectProviderSource"
|
||||
@delete-provider-source="deleteProviderSource" />
|
||||
<div
|
||||
class="provider-sources-column"
|
||||
:class="{ 'mobile-sources': isMobile }"
|
||||
:style="isMobile ? {} : { width: '320px', minWidth: '320px', borderRight: '1px solid rgba(var(--v-border-color), var(--v-border-opacity))', overflowY: 'auto' }"
|
||||
>
|
||||
<ProviderSourcesPanel
|
||||
:displayed-provider-sources="displayedProviderSources"
|
||||
:selected-provider-source="selectedProviderSource"
|
||||
:available-source-types="availableSourceTypes"
|
||||
:tm="tm"
|
||||
:resolve-source-icon="resolveSourceIcon"
|
||||
:get-source-display-name="getSourceDisplayName"
|
||||
@add-provider-source="addProviderSource"
|
||||
@select-provider-source="selectProviderSource"
|
||||
@delete-provider-source="deleteProviderSource"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:配置和模型 -->
|
||||
<div class="provider-config-column" :class="{ 'mobile-config': isMobile }"
|
||||
:style="isMobile ? {} : { flex: 1, overflowY: 'auto', minWidth: 0 }">
|
||||
<div v-if="selectedProviderSource" class="pa-4">
|
||||
<div
|
||||
class="provider-config-column"
|
||||
:class="{ 'mobile-config': isMobile }"
|
||||
:style="isMobile ? {} : { flex: 1, overflowY: 'auto', minWidth: 0 }"
|
||||
>
|
||||
<div
|
||||
v-if="selectedProviderSource"
|
||||
class="pa-4"
|
||||
>
|
||||
<!-- Provider Source 配置 -->
|
||||
<div class="mb-4">
|
||||
<div class="d-flex align-center justify-space-between mb-3">
|
||||
<div>
|
||||
<div class="text-h5 font-weight-bold">{{ selectedProviderSource.id }}</div>
|
||||
<div class="text-caption text-medium-emphasis">{{ selectedProviderSource.api_base || 'N/A' }}</div>
|
||||
<div class="text-h5 font-weight-bold">
|
||||
{{ selectedProviderSource.id }}
|
||||
</div>
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
{{ selectedProviderSource.api_base || 'N/A' }}
|
||||
</div>
|
||||
</div>
|
||||
<v-btn color="success" prepend-icon="mdi-check" :loading="savingSource" :disabled="!isSourceModified"
|
||||
@click="saveProviderSource" variant="flat">
|
||||
<v-btn
|
||||
color="success"
|
||||
prepend-icon="mdi-check"
|
||||
:loading="savingSource"
|
||||
:disabled="!isSourceModified"
|
||||
variant="flat"
|
||||
@click="saveProviderSource"
|
||||
>
|
||||
{{ tm('providerSources.save') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- 基础配置 -->
|
||||
<div class="mb-4">
|
||||
<AstrBotConfig v-if="basicSourceConfig" :iterable="basicSourceConfig" :metadata="configSchema"
|
||||
metadataKey="provider" :is-editing="true" />
|
||||
<AstrBotConfig
|
||||
v-if="basicSourceConfig"
|
||||
:iterable="basicSourceConfig"
|
||||
:metadata="configSchema"
|
||||
metadata-key="provider"
|
||||
:is-editing="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 高级配置 -->
|
||||
<v-expansion-panels variant="accordion" class="mb-4">
|
||||
<v-expansion-panel elevation="0" class="border rounded-lg">
|
||||
<v-expansion-panels
|
||||
variant="accordion"
|
||||
class="mb-4"
|
||||
>
|
||||
<v-expansion-panel
|
||||
elevation="0"
|
||||
class="border rounded-lg"
|
||||
>
|
||||
<v-expansion-panel-title>
|
||||
<span class="font-weight-medium">{{ tm('providerSources.advancedConfig') }}</span>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<AstrBotConfig v-if="advancedSourceConfig" :iterable="advancedSourceConfig"
|
||||
:metadata="configSchema" metadataKey="provider" :is-editing="true" />
|
||||
<AstrBotConfig
|
||||
v-if="advancedSourceConfig"
|
||||
:iterable="advancedSourceConfig"
|
||||
:metadata="configSchema"
|
||||
metadata-key="provider"
|
||||
:is-editing="true"
|
||||
/>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
|
||||
<!-- 模型配置 -->
|
||||
<ProviderModelsPanel :entries="filteredMergedModelEntries" :available-count="availableModels.length"
|
||||
v-model:model-search="modelSearch" :loading-models="loadingModels"
|
||||
:is-source-modified="isSourceModified" :supports-image-input="supportsImageInput"
|
||||
:supports-tool-call="supportsToolCall" :supports-reasoning="supportsReasoning"
|
||||
:format-context-limit="formatContextLimit" :testing-providers="testingProviders" :tm="tm"
|
||||
@fetch-models="fetchAvailableModels" @open-manual-model="openManualModelDialog"
|
||||
@open-provider-edit="openProviderEdit" @toggle-provider-enable="toggleProviderEnable"
|
||||
@test-provider="testProvider" @delete-provider="deleteProvider"
|
||||
@add-model-provider="addModelProvider" />
|
||||
<ProviderModelsPanel
|
||||
v-model:model-search="modelSearch"
|
||||
:entries="filteredMergedModelEntries"
|
||||
:available-count="availableModels.length"
|
||||
:loading-models="loadingModels"
|
||||
:is-source-modified="isSourceModified"
|
||||
:supports-image-input="supportsImageInput"
|
||||
:supports-tool-call="supportsToolCall"
|
||||
:supports-reasoning="supportsReasoning"
|
||||
:format-context-limit="formatContextLimit"
|
||||
:testing-providers="testingProviders"
|
||||
:tm="tm"
|
||||
@fetch-models="fetchAvailableModels"
|
||||
@open-manual-model="openManualModelDialog"
|
||||
@open-provider-edit="openProviderEdit"
|
||||
@toggle-provider-enable="toggleProviderEnable"
|
||||
@test-provider="testProvider"
|
||||
@delete-provider="deleteProvider"
|
||||
@add-model-provider="addModelProvider"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="d-flex align-center justify-center" style="height: 100%;">
|
||||
<div
|
||||
v-else
|
||||
class="d-flex align-center justify-center"
|
||||
style="height: 100%;"
|
||||
>
|
||||
<div class="text-center text-medium-emphasis">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-cursor-default-click</v-icon>
|
||||
<p class="mt-4 text-h6">{{ tm('providerSources.selectHint') }}</p>
|
||||
<v-icon
|
||||
size="64"
|
||||
color="grey-lighten-1"
|
||||
>
|
||||
mdi-cursor-default-click
|
||||
</v-icon>
|
||||
<p class="mt-4 text-h6">
|
||||
{{ tm('providerSources.selectHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,38 +164,77 @@
|
||||
</v-card>
|
||||
|
||||
<!-- 手动添加模型对话框 -->
|
||||
<v-dialog v-model="showManualModelDialog" max-width="400">
|
||||
<v-dialog
|
||||
v-model="showManualModelDialog"
|
||||
max-width="400"
|
||||
>
|
||||
<v-card :title="tm('models.manualDialogTitle')">
|
||||
<v-card-text class="py-4">
|
||||
<v-text-field v-model="manualModelId" :label="tm('models.manualDialogModelLabel')" flat variant="solo-filled"
|
||||
autofocus clearable></v-text-field>
|
||||
<v-text-field :model-value="manualProviderId" flat variant="solo-filled"
|
||||
:label="tm('models.manualDialogPreviewLabel')" persistent-hint
|
||||
:hint="tm('models.manualDialogPreviewHint')"></v-text-field>
|
||||
<v-text-field
|
||||
v-model="manualModelId"
|
||||
:label="tm('models.manualDialogModelLabel')"
|
||||
flat
|
||||
variant="solo-filled"
|
||||
autofocus
|
||||
clearable
|
||||
/>
|
||||
<v-text-field
|
||||
:model-value="manualProviderId"
|
||||
flat
|
||||
variant="solo-filled"
|
||||
:label="tm('models.manualDialogPreviewLabel')"
|
||||
persistent-hint
|
||||
:hint="tm('models.manualDialogPreviewHint')"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="showManualModelDialog = false">取消</v-btn>
|
||||
<v-btn color="primary" @click="confirmManualModel">添加</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="showManualModelDialog = false"
|
||||
>
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="confirmManualModel"
|
||||
>
|
||||
添加
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 已配置模型编辑对话框 -->
|
||||
<v-dialog v-model="showProviderEditDialog" width="800">
|
||||
<v-dialog
|
||||
v-model="showProviderEditDialog"
|
||||
width="800"
|
||||
>
|
||||
<v-card :title="providerEditData?.id || tm('dialogs.config.editTitle')">
|
||||
<v-card-text class="py-4">
|
||||
<small style="color: gray;">不建议修改 ID,可能会导致指向该模型的相关配置(如默认模型、插件相关配置等)失效。</small>
|
||||
<AstrBotConfig v-if="providerEditData" :iterable="providerEditData" :metadata="configSchema"
|
||||
metadataKey="provider" :is-editing="true" />
|
||||
<AstrBotConfig
|
||||
v-if="providerEditData"
|
||||
:iterable="providerEditData"
|
||||
:metadata="configSchema"
|
||||
metadata-key="provider"
|
||||
:is-editing="true"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="showProviderEditDialog = false"
|
||||
:disabled="savingProviders.includes(providerEditData?.id)">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
:disabled="savingProviders.includes(providerEditData?.id)"
|
||||
@click="showProviderEditDialog = false"
|
||||
>
|
||||
{{ tm('dialogs.config.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" @click="saveEditedProvider" :loading="savingProviders.includes(providerEditData?.id)">
|
||||
<v-btn
|
||||
color="primary"
|
||||
:loading="savingProviders.includes(providerEditData?.id)"
|
||||
@click="saveEditedProvider"
|
||||
>
|
||||
{{ tm('dialogs.config.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
|
||||
@@ -1,60 +1,117 @@
|
||||
<template>
|
||||
<v-menu v-model="menuOpen" :close-on-content-click="false" location="top" @update:model-value="handleMenuToggle">
|
||||
<template v-slot:activator="{ props: menuProps }">
|
||||
<v-chip v-bind="menuProps" class="text-none provider-chip" variant="tonal" :size="chipSize">
|
||||
<v-icon start size="14">mdi-creation</v-icon>
|
||||
<span v-if="selectedProviderId">
|
||||
{{ selectedProviderId }}
|
||||
</span>
|
||||
<span v-else>Model</span>
|
||||
</v-chip>
|
||||
</template>
|
||||
<v-card class="provider-menu-card" min-width="280" max-width="400">
|
||||
<v-card-text class="pa-2">
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
placeholder="Search..."
|
||||
hide-details
|
||||
variant="plain"
|
||||
flat
|
||||
density="compact"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
class="ml-2 mb-2 mr-2"
|
||||
clearable
|
||||
/>
|
||||
<v-list density="compact" nav class="provider-menu-list">
|
||||
<v-list-item v-for="provider in filteredProviders" :key="provider.id"
|
||||
:active="selectedProviderId === provider.id" @click="selectProvider(provider)" rounded="lg"
|
||||
class="provider-menu-item">
|
||||
<v-list-item-title class="text-body-2">{{ provider.id }}</v-list-item-title>
|
||||
<v-list-item-subtitle class="provider-subtitle">
|
||||
<span class="model-name">{{ provider.model }}</span>
|
||||
<span class="meta-icons">
|
||||
<v-tooltip text="支持图像输入" location="top" v-if="supportsImageInput(provider)">
|
||||
<template v-slot:activator="{ props: tipProps }">
|
||||
<v-icon v-bind="tipProps" size="12" color="grey">mdi-eye-outline</v-icon>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip text="支持工具调用" location="top" v-if="supportsToolCall(provider)">
|
||||
<template v-slot:activator="{ props: tipProps }">
|
||||
<v-icon v-bind="tipProps" size="12" color="grey">mdi-wrench</v-icon>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip text="支持推理" location="top" v-if="supportsReasoning(provider)">
|
||||
<template v-slot:activator="{ props: tipProps }">
|
||||
<v-icon v-bind="tipProps" size="12" color="grey">mdi-brain</v-icon>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</span>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<div v-if="providerConfigs.length === 0" class="empty-hint">
|
||||
No available models
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
<v-menu
|
||||
v-model="menuOpen"
|
||||
:close-on-content-click="false"
|
||||
location="top"
|
||||
@update:model-value="handleMenuToggle"
|
||||
>
|
||||
<template #activator="{ props: menuProps }">
|
||||
<v-chip
|
||||
v-bind="menuProps"
|
||||
class="text-none provider-chip"
|
||||
variant="tonal"
|
||||
:size="chipSize"
|
||||
>
|
||||
<v-icon
|
||||
start
|
||||
size="14"
|
||||
>
|
||||
mdi-creation
|
||||
</v-icon>
|
||||
<span v-if="selectedProviderId">
|
||||
{{ selectedProviderId }}
|
||||
</span>
|
||||
<span v-else>Model</span>
|
||||
</v-chip>
|
||||
</template>
|
||||
<v-card
|
||||
class="provider-menu-card"
|
||||
min-width="280"
|
||||
max-width="400"
|
||||
>
|
||||
<v-card-text class="pa-2">
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
placeholder="Search..."
|
||||
hide-details
|
||||
variant="plain"
|
||||
flat
|
||||
density="compact"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
class="ml-2 mb-2 mr-2"
|
||||
clearable
|
||||
/>
|
||||
<v-list
|
||||
density="compact"
|
||||
nav
|
||||
class="provider-menu-list"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="provider in filteredProviders"
|
||||
:key="provider.id"
|
||||
:active="selectedProviderId === provider.id"
|
||||
rounded="lg"
|
||||
class="provider-menu-item"
|
||||
@click="selectProvider(provider)"
|
||||
>
|
||||
<v-list-item-title class="text-body-2">
|
||||
{{ provider.id }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="provider-subtitle">
|
||||
<span class="model-name">{{ provider.model }}</span>
|
||||
<span class="meta-icons">
|
||||
<v-tooltip
|
||||
v-if="supportsImageInput(provider)"
|
||||
text="支持图像输入"
|
||||
location="top"
|
||||
>
|
||||
<template #activator="{ props: tipProps }">
|
||||
<v-icon
|
||||
v-bind="tipProps"
|
||||
size="12"
|
||||
color="grey"
|
||||
>mdi-eye-outline</v-icon>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip
|
||||
v-if="supportsToolCall(provider)"
|
||||
text="支持工具调用"
|
||||
location="top"
|
||||
>
|
||||
<template #activator="{ props: tipProps }">
|
||||
<v-icon
|
||||
v-bind="tipProps"
|
||||
size="12"
|
||||
color="grey"
|
||||
>mdi-wrench</v-icon>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip
|
||||
v-if="supportsReasoning(provider)"
|
||||
text="支持推理"
|
||||
location="top"
|
||||
>
|
||||
<template #activator="{ props: tipProps }">
|
||||
<v-icon
|
||||
v-bind="tipProps"
|
||||
size="12"
|
||||
color="grey"
|
||||
>mdi-brain</v-icon>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</span>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<div
|
||||
v-if="providerConfigs.length === 0"
|
||||
class="empty-hint"
|
||||
>
|
||||
No available models
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
<template>
|
||||
<v-card class="standalone-chat-card" elevation="0" rounded="0">
|
||||
<v-card
|
||||
class="standalone-chat-card"
|
||||
elevation="0"
|
||||
rounded="0"
|
||||
>
|
||||
<v-card-text class="standalone-chat-container">
|
||||
<div class="chat-layout">
|
||||
<!-- 聊天内容区域 -->
|
||||
<div class="chat-content-panel">
|
||||
<MessageList
|
||||
v-if="messages && messages.length > 0"
|
||||
:messages="messages"
|
||||
:isDark="isDark"
|
||||
:isStreaming="isStreaming || isConvRunning"
|
||||
@openImagePreview="openImagePreview"
|
||||
ref="messageList"
|
||||
:messages="messages"
|
||||
:is-dark="isDark"
|
||||
:is-streaming="isStreaming || isConvRunning"
|
||||
@openImagePreview="openImagePreview"
|
||||
/>
|
||||
<div class="welcome-container fade-in" v-else>
|
||||
<div
|
||||
v-else
|
||||
class="welcome-container fade-in"
|
||||
>
|
||||
<div class="welcome-title">
|
||||
<span>Hello, I'm</span>
|
||||
<span class="bot-name">AstrBot ⭐</span>
|
||||
@@ -24,13 +31,14 @@
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<ChatInput
|
||||
ref="chatInputRef"
|
||||
v-model:prompt="prompt"
|
||||
:stagedImagesUrl="stagedImagesUrl"
|
||||
:stagedAudioUrl="stagedAudioUrl"
|
||||
:staged-images-url="stagedImagesUrl"
|
||||
:staged-audio-url="stagedAudioUrl"
|
||||
:disabled="isStreaming"
|
||||
:is-running="isStreaming || isConvRunning"
|
||||
:enableStreaming="enableStreaming"
|
||||
:isRecording="isRecording"
|
||||
:enable-streaming="enableStreaming"
|
||||
:is-recording="isRecording"
|
||||
:session-id="currSessionId || null"
|
||||
:current-session="getCurrentSession"
|
||||
:config-id="configId"
|
||||
@@ -43,8 +51,6 @@
|
||||
@stopRecording="handleStopRecording"
|
||||
@pasteImage="handlePaste"
|
||||
@fileSelect="handleFileSelect"
|
||||
@openLiveMode=""
|
||||
ref="chatInputRef"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -52,8 +58,15 @@
|
||||
</v-card>
|
||||
|
||||
<!-- 图片预览对话框 -->
|
||||
<v-dialog v-model="imagePreviewDialog" max-width="90vw" max-height="90vh">
|
||||
<v-card class="image-preview-card" elevation="8">
|
||||
<v-dialog
|
||||
v-model="imagePreviewDialog"
|
||||
max-width="90vw"
|
||||
max-height="90vh"
|
||||
>
|
||||
<v-card
|
||||
class="image-preview-card"
|
||||
elevation="8"
|
||||
>
|
||||
<v-card-title class="d-flex justify-space-between align-center pa-4">
|
||||
<span>{{ t("core.common.imagePreview") }}</span>
|
||||
<v-btn
|
||||
@@ -63,7 +76,10 @@
|
||||
/>
|
||||
</v-card-title>
|
||||
<v-card-text class="text-center pa-4">
|
||||
<img :src="previewImageUrl" class="preview-image-large" />
|
||||
<img
|
||||
:src="previewImageUrl"
|
||||
class="preview-image-large"
|
||||
>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
@@ -1,29 +1,32 @@
|
||||
<template>
|
||||
<div class="welcome-container fade-in">
|
||||
<div v-if="isLoading" class="loading-overlay-welcome">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
size="48"
|
||||
width="4"
|
||||
color="primary"
|
||||
></v-progress-circular>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="welcome-content">
|
||||
<div class="welcome-title">
|
||||
<span class="bot-name-container">
|
||||
<span class="bot-name-text">
|
||||
Hello, I'm <span class="highlight-name">AstrBot</span>
|
||||
</span>
|
||||
<span class="bot-name-star">⭐</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="welcome-input">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<div class="welcome-container fade-in">
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="loading-overlay-welcome"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
size="48"
|
||||
width="4"
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="welcome-content">
|
||||
<div class="welcome-title">
|
||||
<span class="bot-name-container">
|
||||
<span class="bot-name-text">
|
||||
Hello, I'm <span class="highlight-name">AstrBot</span>
|
||||
</span>
|
||||
<span class="bot-name-star">⭐</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="welcome-input">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,20 +1,41 @@
|
||||
<template>
|
||||
<div v-if="refs && refs.used && refs.used.length > 0" class="refs-container" @click="handleClick">
|
||||
<div class="refs-avatars">
|
||||
<div v-for="(ref, refIdx) in refs.used.slice(0, 3)" :key="refIdx" class="ref-avatar"
|
||||
:style="{ zIndex: 3 - refIdx }">
|
||||
<img v-if="ref.favicon" :src="ref.favicon" class="ref-favicon"
|
||||
@error="(e) => e.target.style.display = 'none'" />
|
||||
<span v-else class="ref-initial">{{ getRefInitial(ref.title) }}</span>
|
||||
</div>
|
||||
<span v-if="refs.used.length > 3" class="refs-more">
|
||||
+{{ refs.used.length - 3 }}
|
||||
</span>
|
||||
<span class="ml-2" style="color: gray;">
|
||||
{{ tm('refs.sources') }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="refs && refs.used && refs.used.length > 0"
|
||||
class="refs-container"
|
||||
@click="handleClick"
|
||||
>
|
||||
<div class="refs-avatars">
|
||||
<div
|
||||
v-for="(ref, refIdx) in refs.used.slice(0, 3)"
|
||||
:key="refIdx"
|
||||
class="ref-avatar"
|
||||
:style="{ zIndex: 3 - refIdx }"
|
||||
>
|
||||
<img
|
||||
v-if="ref.favicon"
|
||||
:src="ref.favicon"
|
||||
class="ref-favicon"
|
||||
@error="(e) => e.target.style.display = 'none'"
|
||||
>
|
||||
<span
|
||||
v-else
|
||||
class="ref-initial"
|
||||
>{{ getRefInitial(ref.title) }}</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="refs.used.length > 3"
|
||||
class="refs-more"
|
||||
>
|
||||
+{{ refs.used.length - 3 }}
|
||||
</span>
|
||||
<span
|
||||
class="ml-2"
|
||||
style="color: gray;"
|
||||
>
|
||||
{{ tm('refs.sources') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,24 +1,41 @@
|
||||
<template>
|
||||
<div class="ipython-tool-block" :class="{ compact: !showHeader }">
|
||||
<div v-if="displayExpanded" class="py-3 animate-fade-in">
|
||||
<!-- Code Section -->
|
||||
<div class="code-section">
|
||||
<div v-if="shikiReady && code" class="code-highlighted"
|
||||
v-html="highlightedCode"></div>
|
||||
<pre v-else class="code-fallback"
|
||||
:class="{ 'dark-theme': isDark }">{{ code || 'No code available' }}</pre>
|
||||
</div>
|
||||
<div
|
||||
class="ipython-tool-block"
|
||||
:class="{ compact: !showHeader }"
|
||||
>
|
||||
<div
|
||||
v-if="displayExpanded"
|
||||
class="py-3 animate-fade-in"
|
||||
>
|
||||
<!-- Code Section -->
|
||||
<div class="code-section">
|
||||
<div
|
||||
v-if="shikiReady && code"
|
||||
class="code-highlighted"
|
||||
v-html="highlightedCode"
|
||||
/>
|
||||
<pre
|
||||
v-else
|
||||
class="code-fallback"
|
||||
:class="{ 'dark-theme': isDark }"
|
||||
>{{ code || 'No code available' }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- Result Section -->
|
||||
<div v-if="result" class="result-section">
|
||||
<div class="result-label">
|
||||
{{ tm('ipython.output') }}:
|
||||
</div>
|
||||
<pre class="result-content"
|
||||
:class="{ 'dark-theme': isDark }">{{ formattedResult }}</pre>
|
||||
</div>
|
||||
<!-- Result Section -->
|
||||
<div
|
||||
v-if="result"
|
||||
class="result-section"
|
||||
>
|
||||
<div class="result-label">
|
||||
{{ tm('ipython.output') }}:
|
||||
</div>
|
||||
<pre
|
||||
class="result-content"
|
||||
:class="{ 'dark-theme': isDark }"
|
||||
>{{ formattedResult }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -1,118 +1,207 @@
|
||||
<template>
|
||||
<template v-for="(renderPart, renderIndex) in getRenderParts(parts)" :key="renderPart.key">
|
||||
<!-- Grouped Tool Calls (consecutive tool_call parts) -->
|
||||
<div v-if="renderPart.type === 'tool_group'" class="tool-call-compact">
|
||||
<transition-group name="tool-call-item" tag="div" class="tool-call-items">
|
||||
<ToolCallItem v-for="(toolCall, tcIndex) in renderPart.toolCalls" :key="toolCall.id" :is-dark="isDark">
|
||||
<template #label="{ expanded }">
|
||||
<v-icon size="x-small" v-if="toolCall.name.includes('web_search') || toolCall.name.includes('tavily')">
|
||||
mdi-web
|
||||
</v-icon>
|
||||
<v-icon size="x-small" v-else-if="toolCall.name === 'astrbot_execute_shell'">
|
||||
mdi-console-line
|
||||
</v-icon>
|
||||
<v-icon size="x-small" v-else>
|
||||
mdi-wrench
|
||||
</v-icon>
|
||||
{{ tm('actions.toolCallUsed', { name: toolCall.name }) }}
|
||||
<span style="opacity: 0.6;">{{ toolCall.finished_ts ? formatDuration(toolCall.finished_ts -
|
||||
toolCall.ts) : getElapsedTime(toolCall.ts) }}</span>
|
||||
<v-icon size="x-small" class="tool-call-chevron" :class="{ rotated: expanded }">
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
</template>
|
||||
<template #details>
|
||||
<div class="tool-call-detail-row">
|
||||
<span class="detail-label">ID:</span>
|
||||
<code class="detail-value">{{ toolCall.id }}</code>
|
||||
</div>
|
||||
<div class="tool-call-detail-row">
|
||||
<span class="detail-label">Args:</span>
|
||||
<pre class="detail-value detail-json">{{ formatToolArgs(toolCall.args) }}</pre>
|
||||
</div>
|
||||
<div v-if="toolCall.result" class="tool-call-detail-row">
|
||||
<span class="detail-label">Result:</span>
|
||||
<pre
|
||||
class="detail-value detail-json detail-result">{{ formatToolResult(toolCall.result) }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
</ToolCallItem>
|
||||
</transition-group>
|
||||
</div>
|
||||
|
||||
<!-- iPython Tool Block -->
|
||||
<ToolCallItem v-else-if="renderPart.type === 'ipython'" :is-dark="isDark" style="margin: 8px 0 4px;">
|
||||
<template #label="{ expanded }">
|
||||
<v-icon size="x-small">
|
||||
mdi-code-json
|
||||
</v-icon>
|
||||
<span class="ipython-label">{{ tm('actions.pythonCodeAnalysis') }}</span>
|
||||
<span style="opacity: 0.6;">{{ renderPart.toolCall.finished_ts ?
|
||||
formatDuration(renderPart.toolCall.finished_ts -
|
||||
renderPart.toolCall.ts) : getElapsedTime(renderPart.toolCall.ts) }}</span>
|
||||
<v-icon size="small" class="ipython-icon" :class="{ rotated: expanded }">
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
</template>
|
||||
<template #details>
|
||||
<IPythonToolBlock :tool-call="renderPart.toolCall" :is-dark="isDark" :show-header="false"
|
||||
:force-expanded="true" />
|
||||
</template>
|
||||
<template
|
||||
v-for="(renderPart, renderIndex) in getRenderParts(parts)"
|
||||
:key="renderPart.key"
|
||||
>
|
||||
<!-- Grouped Tool Calls (consecutive tool_call parts) -->
|
||||
<div
|
||||
v-if="renderPart.type === 'tool_group'"
|
||||
class="tool-call-compact"
|
||||
>
|
||||
<transition-group
|
||||
name="tool-call-item"
|
||||
tag="div"
|
||||
class="tool-call-items"
|
||||
>
|
||||
<ToolCallItem
|
||||
v-for="(toolCall, tcIndex) in renderPart.toolCalls"
|
||||
:key="toolCall.id"
|
||||
:is-dark="isDark"
|
||||
>
|
||||
<template #label="{ expanded }">
|
||||
<v-icon
|
||||
v-if="toolCall.name.includes('web_search') || toolCall.name.includes('tavily')"
|
||||
size="x-small"
|
||||
>
|
||||
mdi-web
|
||||
</v-icon>
|
||||
<v-icon
|
||||
v-else-if="toolCall.name === 'astrbot_execute_shell'"
|
||||
size="x-small"
|
||||
>
|
||||
mdi-console-line
|
||||
</v-icon>
|
||||
<v-icon
|
||||
v-else
|
||||
size="x-small"
|
||||
>
|
||||
mdi-wrench
|
||||
</v-icon>
|
||||
{{ tm('actions.toolCallUsed', { name: toolCall.name }) }}
|
||||
<span style="opacity: 0.6;">{{ toolCall.finished_ts ? formatDuration(toolCall.finished_ts -
|
||||
toolCall.ts) : getElapsedTime(toolCall.ts) }}</span>
|
||||
<v-icon
|
||||
size="x-small"
|
||||
class="tool-call-chevron"
|
||||
:class="{ rotated: expanded }"
|
||||
>
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
</template>
|
||||
<template #details>
|
||||
<div class="tool-call-detail-row">
|
||||
<span class="detail-label">ID:</span>
|
||||
<code class="detail-value">{{ toolCall.id }}</code>
|
||||
</div>
|
||||
<div class="tool-call-detail-row">
|
||||
<span class="detail-label">Args:</span>
|
||||
<pre class="detail-value detail-json">{{ formatToolArgs(toolCall.args) }}</pre>
|
||||
</div>
|
||||
<div
|
||||
v-if="toolCall.result"
|
||||
class="tool-call-detail-row"
|
||||
>
|
||||
<span class="detail-label">Result:</span>
|
||||
<pre
|
||||
class="detail-value detail-json detail-result"
|
||||
>{{ formatToolResult(toolCall.result) }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
</ToolCallItem>
|
||||
</transition-group>
|
||||
</div>
|
||||
|
||||
<!-- Text (Markdown) -->
|
||||
<MarkdownRender
|
||||
v-else-if="renderPart.part.type === 'plain' && renderPart.part.text && renderPart.part.text.trim()"
|
||||
:key="`${renderPart.key}-${isDark ? 'dark' : 'light'}`"
|
||||
custom-id="message-list" :custom-html-tags="['ref']" :content="renderPart.part.text" :typewriter="false"
|
||||
class="markdown-content" :is-dark="isDark" />
|
||||
<!-- iPython Tool Block -->
|
||||
<ToolCallItem
|
||||
v-else-if="renderPart.type === 'ipython'"
|
||||
:is-dark="isDark"
|
||||
style="margin: 8px 0 4px;"
|
||||
>
|
||||
<template #label="{ expanded }">
|
||||
<v-icon size="x-small">
|
||||
mdi-code-json
|
||||
</v-icon>
|
||||
<span class="ipython-label">{{ tm('actions.pythonCodeAnalysis') }}</span>
|
||||
<span style="opacity: 0.6;">{{ renderPart.toolCall.finished_ts ?
|
||||
formatDuration(renderPart.toolCall.finished_ts -
|
||||
renderPart.toolCall.ts) : getElapsedTime(renderPart.toolCall.ts) }}</span>
|
||||
<v-icon
|
||||
size="small"
|
||||
class="ipython-icon"
|
||||
:class="{ rotated: expanded }"
|
||||
>
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
</template>
|
||||
<template #details>
|
||||
<IPythonToolBlock
|
||||
:tool-call="renderPart.toolCall"
|
||||
:is-dark="isDark"
|
||||
:show-header="false"
|
||||
:force-expanded="true"
|
||||
/>
|
||||
</template>
|
||||
</ToolCallItem>
|
||||
|
||||
<!-- Image -->
|
||||
<div v-else-if="renderPart.part.type === 'image' && renderPart.part.embedded_url" class="embedded-images">
|
||||
<div class="embedded-image">
|
||||
<img :src="renderPart.part.embedded_url" class="bot-embedded-image"
|
||||
@click="emitOpenImage(renderPart.part.embedded_url)" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Text (Markdown) -->
|
||||
<MarkdownRender
|
||||
v-else-if="renderPart.part.type === 'plain' && renderPart.part.text && renderPart.part.text.trim()"
|
||||
:key="`${renderPart.key}-${isDark ? 'dark' : 'light'}`"
|
||||
custom-id="message-list"
|
||||
:custom-html-tags="['ref']"
|
||||
:content="renderPart.part.text"
|
||||
:typewriter="false"
|
||||
class="markdown-content"
|
||||
:is-dark="isDark"
|
||||
/>
|
||||
|
||||
<!-- Audio -->
|
||||
<div v-else-if="renderPart.part.type === 'record' && renderPart.part.embedded_url" class="embedded-audio">
|
||||
<audio controls class="audio-player">
|
||||
<source :src="renderPart.part.embedded_url" type="audio/wav">
|
||||
{{ t('messages.errors.browser.audioNotSupported') }}
|
||||
</audio>
|
||||
</div>
|
||||
<!-- Image -->
|
||||
<div
|
||||
v-else-if="renderPart.part.type === 'image' && renderPart.part.embedded_url"
|
||||
class="embedded-images"
|
||||
>
|
||||
<div class="embedded-image">
|
||||
<img
|
||||
:src="renderPart.part.embedded_url"
|
||||
class="bot-embedded-image"
|
||||
@click="emitOpenImage(renderPart.part.embedded_url)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Files -->
|
||||
<div v-else-if="renderPart.part.type === 'file' && renderPart.part.embedded_file" class="embedded-files">
|
||||
<div class="embedded-file">
|
||||
<a v-if="renderPart.part.embedded_file.url" :href="renderPart.part.embedded_file.url"
|
||||
:download="renderPart.part.embedded_file.filename" class="file-link" :class="{ 'is-dark': isDark }"
|
||||
:style="isDark ? {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||
color: 'var(--v-theme-secondary)'
|
||||
} : {}">
|
||||
<v-icon size="small" class="file-icon"
|
||||
:style="isDark ? { color: 'var(--v-theme-secondary)' } : {}">mdi-file-document-outline</v-icon>
|
||||
<span class="file-name">{{ renderPart.part.embedded_file.filename }}</span>
|
||||
</a>
|
||||
<a v-else @click="emitDownloadFile(renderPart.part.embedded_file)" class="file-link file-link-download"
|
||||
:class="{ 'is-dark': isDark }" :style="isDark ? {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||
color: 'var(--v-theme-secondary)'
|
||||
} : {}">
|
||||
<v-icon size="small" class="file-icon"
|
||||
:style="isDark ? { color: 'var(--v-theme-secondary)' } : {}">mdi-file-document-outline</v-icon>
|
||||
<span class="file-name">{{ renderPart.part.embedded_file.filename }}</span>
|
||||
<v-icon v-if="downloadingFiles?.has(renderPart.part.embedded_file.attachment_id)" size="small"
|
||||
class="download-icon">mdi-loading mdi-spin</v-icon>
|
||||
<v-icon v-else size="small" class="download-icon">mdi-download</v-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Audio -->
|
||||
<div
|
||||
v-else-if="renderPart.part.type === 'record' && renderPart.part.embedded_url"
|
||||
class="embedded-audio"
|
||||
>
|
||||
<audio
|
||||
controls
|
||||
class="audio-player"
|
||||
>
|
||||
<source
|
||||
:src="renderPart.part.embedded_url"
|
||||
type="audio/wav"
|
||||
>
|
||||
{{ t('messages.errors.browser.audioNotSupported') }}
|
||||
</audio>
|
||||
</div>
|
||||
|
||||
<!-- Files -->
|
||||
<div
|
||||
v-else-if="renderPart.part.type === 'file' && renderPart.part.embedded_file"
|
||||
class="embedded-files"
|
||||
>
|
||||
<div class="embedded-file">
|
||||
<a
|
||||
v-if="renderPart.part.embedded_file.url"
|
||||
:href="renderPart.part.embedded_file.url"
|
||||
:download="renderPart.part.embedded_file.filename"
|
||||
class="file-link"
|
||||
:class="{ 'is-dark': isDark }"
|
||||
:style="isDark ? {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||
color: 'var(--v-theme-secondary)'
|
||||
} : {}"
|
||||
>
|
||||
<v-icon
|
||||
size="small"
|
||||
class="file-icon"
|
||||
:style="isDark ? { color: 'var(--v-theme-secondary)' } : {}"
|
||||
>mdi-file-document-outline</v-icon>
|
||||
<span class="file-name">{{ renderPart.part.embedded_file.filename }}</span>
|
||||
</a>
|
||||
<a
|
||||
v-else
|
||||
class="file-link file-link-download"
|
||||
:class="{ 'is-dark': isDark }"
|
||||
:style="isDark ? {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||
color: 'var(--v-theme-secondary)'
|
||||
} : {}"
|
||||
@click="emitDownloadFile(renderPart.part.embedded_file)"
|
||||
>
|
||||
<v-icon
|
||||
size="small"
|
||||
class="file-icon"
|
||||
:style="isDark ? { color: 'var(--v-theme-secondary)' } : {}"
|
||||
>mdi-file-document-outline</v-icon>
|
||||
<span class="file-name">{{ renderPart.part.embedded_file.filename }}</span>
|
||||
<v-icon
|
||||
v-if="downloadingFiles?.has(renderPart.part.embedded_file.attachment_id)"
|
||||
size="small"
|
||||
class="download-icon"
|
||||
>mdi-loading mdi-spin</v-icon>
|
||||
<v-icon
|
||||
v-else
|
||||
size="small"
|
||||
class="download-icon"
|
||||
>mdi-download</v-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -1,18 +1,37 @@
|
||||
<template>
|
||||
<div class="reasoning-block" :class="{ 'reasoning-block--dark': isDark }">
|
||||
<div class="reasoning-header" @click="toggleExpanded">
|
||||
<v-icon size="small" class="reasoning-icon" :class="{ 'rotate-90': isExpanded }">
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
<span class="reasoning-title">
|
||||
{{ tm('reasoning.thinking') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="isExpanded" class="reasoning-content animate-fade-in">
|
||||
<MarkdownRender :key="`reasoning-${isDark ? 'dark' : 'light'}`" :content="reasoning" class="reasoning-text markdown-content"
|
||||
:typewriter="false" :is-dark="isDark" :style="isDark ? { opacity: '0.85' } : {}" />
|
||||
</div>
|
||||
<div
|
||||
class="reasoning-block"
|
||||
:class="{ 'reasoning-block--dark': isDark }"
|
||||
>
|
||||
<div
|
||||
class="reasoning-header"
|
||||
@click="toggleExpanded"
|
||||
>
|
||||
<v-icon
|
||||
size="small"
|
||||
class="reasoning-icon"
|
||||
:class="{ 'rotate-90': isExpanded }"
|
||||
>
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
<span class="reasoning-title">
|
||||
{{ tm('reasoning.thinking') }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="isExpanded"
|
||||
class="reasoning-content animate-fade-in"
|
||||
>
|
||||
<MarkdownRender
|
||||
:key="`reasoning-${isDark ? 'dark' : 'light'}`"
|
||||
:content="reasoning"
|
||||
class="reasoning-text markdown-content"
|
||||
:typewriter="false"
|
||||
:is-dark="isDark"
|
||||
:style="isDark ? { opacity: '0.85' } : {}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
<template>
|
||||
<v-chip v-if="domain" class="ref-chip" size="x-small" variant="flat"
|
||||
:style="chipStyle" :href="url"
|
||||
target="_blank" clickable>
|
||||
<v-icon start size="x-small" color>mdi-link-variant</v-icon>
|
||||
<span>{{ domain }}</span>
|
||||
|
||||
</v-chip>
|
||||
<span v-else class="ref-fallback" :style="fallbackStyle">{{ 'site' }}</span>
|
||||
<v-chip
|
||||
v-if="domain"
|
||||
class="ref-chip"
|
||||
size="x-small"
|
||||
variant="flat"
|
||||
:style="chipStyle"
|
||||
:href="url"
|
||||
target="_blank"
|
||||
clickable
|
||||
>
|
||||
<v-icon
|
||||
start
|
||||
size="x-small"
|
||||
color
|
||||
>
|
||||
mdi-link-variant
|
||||
</v-icon>
|
||||
<span>{{ domain }}</span>
|
||||
</v-chip>
|
||||
<span
|
||||
v-else
|
||||
class="ref-fallback"
|
||||
:style="fallbackStyle"
|
||||
>{{ 'site' }}</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -1,28 +1,66 @@
|
||||
<template>
|
||||
<transition name="slide-left">
|
||||
<div v-if="isOpen" class="refs-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h3 class="sidebar-title">{{ tm('refs.title') }}</h3>
|
||||
<v-btn icon="mdi-close" size="small" variant="text" @click="close"></v-btn>
|
||||
</div>
|
||||
<transition name="slide-left">
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="refs-sidebar"
|
||||
>
|
||||
<div class="sidebar-header">
|
||||
<h3 class="sidebar-title">
|
||||
{{ tm('refs.title') }}
|
||||
</h3>
|
||||
<v-btn
|
||||
icon="mdi-close"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="close"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="refs-list">
|
||||
<div v-for="(ref, index) in refs?.used || []" :key="index" class="ref-item" @click="openLink(ref.url)">
|
||||
<div class="ref-item-icon">
|
||||
<img v-if="ref.favicon" :src="ref.favicon" class="ref-item-favicon"
|
||||
@error="(e) => e.target.style.display = 'none'" />
|
||||
<div v-else class="ref-item-initial">{{ getRefInitial(ref.title) }}</div>
|
||||
</div>
|
||||
<div class="ref-item-content">
|
||||
<div class="ref-item-title">{{ ref.title }}</div>
|
||||
<div class="ref-item-url">{{ formatUrl(ref.url) }}</div>
|
||||
<div v-if="ref.snippet" class="ref-item-snippet">{{ ref.snippet }}</div>
|
||||
</div>
|
||||
<v-icon size="small" class="ref-item-arrow">mdi-open-in-new</v-icon>
|
||||
</div>
|
||||
<div class="refs-list">
|
||||
<div
|
||||
v-for="(ref, index) in refs?.used || []"
|
||||
:key="index"
|
||||
class="ref-item"
|
||||
@click="openLink(ref.url)"
|
||||
>
|
||||
<div class="ref-item-icon">
|
||||
<img
|
||||
v-if="ref.favicon"
|
||||
:src="ref.favicon"
|
||||
class="ref-item-favicon"
|
||||
@error="(e) => e.target.style.display = 'none'"
|
||||
>
|
||||
<div
|
||||
v-else
|
||||
class="ref-item-initial"
|
||||
>
|
||||
{{ getRefInitial(ref.title) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ref-item-content">
|
||||
<div class="ref-item-title">
|
||||
{{ ref.title }}
|
||||
</div>
|
||||
<div class="ref-item-url">
|
||||
{{ formatUrl(ref.url) }}
|
||||
</div>
|
||||
<div
|
||||
v-if="ref.snippet"
|
||||
class="ref-item-snippet"
|
||||
>
|
||||
{{ ref.snippet }}
|
||||
</div>
|
||||
</div>
|
||||
<v-icon
|
||||
size="small"
|
||||
class="ref-item-arrow"
|
||||
>
|
||||
mdi-open-in-new
|
||||
</v-icon>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,59 +1,99 @@
|
||||
<template>
|
||||
<div class="tool-call-card" :class="{ 'is-dark': isDark, 'expanded': isExpanded }" :style="isDark ? {
|
||||
backgroundColor: 'rgba(40, 60, 100, 0.4)',
|
||||
borderColor: 'rgba(100, 140, 200, 0.4)'
|
||||
} : {}">
|
||||
<!-- Header -->
|
||||
<div class="tool-call-header" :class="{ 'is-dark': isDark }" @click="toggleExpanded">
|
||||
<v-icon size="small" class="tool-call-expand-icon" :class="{ 'expanded': isExpanded }">
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
<v-icon size="small" class="tool-call-icon">mdi-wrench-outline</v-icon>
|
||||
<div class="tool-call-info">
|
||||
<span class="tool-call-name">{{ toolCall.name }}</span>
|
||||
</div>
|
||||
<span class="tool-call-status"
|
||||
:class="{ 'status-running': !toolCall.finished_ts, 'status-finished': toolCall.finished_ts }">
|
||||
<template v-if="toolCall.finished_ts">
|
||||
<v-icon size="x-small" class="status-icon">mdi-check-circle</v-icon>
|
||||
{{ formatDuration(toolCall.finished_ts - toolCall.ts) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-icon size="x-small" class="status-icon spinning">mdi-loading</v-icon>
|
||||
{{ elapsedTime }}
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Details -->
|
||||
<div v-if="isExpanded" class="tool-call-details" :style="isDark ? {
|
||||
borderTopColor: 'rgba(100, 140, 200, 0.3)',
|
||||
backgroundColor: 'rgba(30, 45, 70, 0.5)'
|
||||
} : {}">
|
||||
<!-- ID -->
|
||||
<div class="tool-call-detail-row">
|
||||
<span class="detail-label">ID:</span>
|
||||
<code class="detail-value" :style="isDark ? { backgroundColor: 'transparent' } : {}">
|
||||
{{ toolCall.id }}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<!-- Args -->
|
||||
<div class="tool-call-detail-row">
|
||||
<span class="detail-label">Args:</span>
|
||||
<pre class="detail-value detail-json" :style="isDark ? { backgroundColor: 'transparent' } : {}">{{
|
||||
JSON.stringify(toolCall.args, null, 2) }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- Result -->
|
||||
<div v-if="toolCall.result" class="tool-call-detail-row">
|
||||
<span class="detail-label">Result:</span>
|
||||
<pre class="detail-value detail-json detail-result"
|
||||
:style="isDark ? { backgroundColor: 'transparent' } : {}">{{
|
||||
formattedResult }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tool-call-card"
|
||||
:class="{ 'is-dark': isDark, 'expanded': isExpanded }"
|
||||
:style="isDark ? {
|
||||
backgroundColor: 'rgba(40, 60, 100, 0.4)',
|
||||
borderColor: 'rgba(100, 140, 200, 0.4)'
|
||||
} : {}"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div
|
||||
class="tool-call-header"
|
||||
:class="{ 'is-dark': isDark }"
|
||||
@click="toggleExpanded"
|
||||
>
|
||||
<v-icon
|
||||
size="small"
|
||||
class="tool-call-expand-icon"
|
||||
:class="{ 'expanded': isExpanded }"
|
||||
>
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
<v-icon
|
||||
size="small"
|
||||
class="tool-call-icon"
|
||||
>
|
||||
mdi-wrench-outline
|
||||
</v-icon>
|
||||
<div class="tool-call-info">
|
||||
<span class="tool-call-name">{{ toolCall.name }}</span>
|
||||
</div>
|
||||
<span
|
||||
class="tool-call-status"
|
||||
:class="{ 'status-running': !toolCall.finished_ts, 'status-finished': toolCall.finished_ts }"
|
||||
>
|
||||
<template v-if="toolCall.finished_ts">
|
||||
<v-icon
|
||||
size="x-small"
|
||||
class="status-icon"
|
||||
>mdi-check-circle</v-icon>
|
||||
{{ formatDuration(toolCall.finished_ts - toolCall.ts) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-icon
|
||||
size="x-small"
|
||||
class="status-icon spinning"
|
||||
>mdi-loading</v-icon>
|
||||
{{ elapsedTime }}
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Details -->
|
||||
<div
|
||||
v-if="isExpanded"
|
||||
class="tool-call-details"
|
||||
:style="isDark ? {
|
||||
borderTopColor: 'rgba(100, 140, 200, 0.3)',
|
||||
backgroundColor: 'rgba(30, 45, 70, 0.5)'
|
||||
} : {}"
|
||||
>
|
||||
<!-- ID -->
|
||||
<div class="tool-call-detail-row">
|
||||
<span class="detail-label">ID:</span>
|
||||
<code
|
||||
class="detail-value"
|
||||
:style="isDark ? { backgroundColor: 'transparent' } : {}"
|
||||
>
|
||||
{{ toolCall.id }}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<!-- Args -->
|
||||
<div class="tool-call-detail-row">
|
||||
<span class="detail-label">Args:</span>
|
||||
<pre
|
||||
class="detail-value detail-json"
|
||||
:style="isDark ? { backgroundColor: 'transparent' } : {}"
|
||||
>{{
|
||||
JSON.stringify(toolCall.args, null, 2) }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- Result -->
|
||||
<div
|
||||
v-if="toolCall.result"
|
||||
class="tool-call-detail-row"
|
||||
>
|
||||
<span class="detail-label">Result:</span>
|
||||
<pre
|
||||
class="detail-value detail-json detail-result"
|
||||
:style="isDark ? { backgroundColor: 'transparent' } : {}"
|
||||
>{{
|
||||
formattedResult }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
<template>
|
||||
<div class="tool-call-item">
|
||||
<div class="tool-call-line" role="button" tabindex="0"
|
||||
@click="toggleExpanded"
|
||||
@keydown.enter="toggleExpanded"
|
||||
@keydown.space.prevent="toggleExpanded">
|
||||
<slot name="label" :expanded="isExpanded" />
|
||||
</div>
|
||||
<transition name="tool-call-fade">
|
||||
<div v-if="isExpanded" class="tool-call-inline-details" :class="{ 'is-dark': isDark }">
|
||||
<slot name="details" />
|
||||
</div>
|
||||
</transition>
|
||||
<div class="tool-call-item">
|
||||
<div
|
||||
class="tool-call-line"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="toggleExpanded"
|
||||
@keydown.enter="toggleExpanded"
|
||||
@keydown.space.prevent="toggleExpanded"
|
||||
>
|
||||
<slot
|
||||
name="label"
|
||||
:expanded="isExpanded"
|
||||
/>
|
||||
</div>
|
||||
<transition name="tool-call-fade">
|
||||
<div
|
||||
v-if="isExpanded"
|
||||
class="tool-call-inline-details"
|
||||
:class="{ 'is-dark': isDark }"
|
||||
>
|
||||
<slot name="details" />
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -1,45 +1,75 @@
|
||||
<template>
|
||||
<div :class="$vuetify.display.mobile ? '' : 'd-flex'">
|
||||
<v-tabs v-model="tab" :direction="$vuetify.display.mobile ? 'horizontal' : 'vertical'"
|
||||
:align-tabs="$vuetify.display.mobile ? 'left' : 'start'" color="deep-purple-accent-4" class="config-tabs">
|
||||
<v-tab v-for="section in visibleSections" :key="section.key" :value="section.key"
|
||||
style="font-weight: 1000; font-size: 15px">
|
||||
{{ tm(section.value['name']) }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<v-tabs-window v-model="tab" class="config-tabs-window" :style="readonly ? 'pointer-events: none; opacity: 0.6;' : ''">
|
||||
<v-tabs-window-item v-for="section in visibleSections" :key="section.key" :value="section.key">
|
||||
<v-container fluid>
|
||||
<div v-for="(val2, key2, index2) in section.value['metadata']" :key="key2">
|
||||
<!-- Support both traditional and JSON selector metadata -->
|
||||
<AstrBotConfigV4
|
||||
:metadata="{ [key2]: section.value['metadata'][key2] }"
|
||||
:iterable="config_data"
|
||||
:metadataKey="key2"
|
||||
:search-keyword="searchKeyword"
|
||||
>
|
||||
</AstrBotConfigV4>
|
||||
</div>
|
||||
</v-container>
|
||||
</v-tabs-window-item>
|
||||
<div :class="$vuetify.display.mobile ? '' : 'd-flex'">
|
||||
<v-tabs
|
||||
v-model="tab"
|
||||
:direction="$vuetify.display.mobile ? 'horizontal' : 'vertical'"
|
||||
:align-tabs="$vuetify.display.mobile ? 'left' : 'start'"
|
||||
color="deep-purple-accent-4"
|
||||
class="config-tabs"
|
||||
>
|
||||
<v-tab
|
||||
v-for="section in visibleSections"
|
||||
:key="section.key"
|
||||
:value="section.key"
|
||||
style="font-weight: 1000; font-size: 15px"
|
||||
>
|
||||
{{ tm(section.value['name']) }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<v-tabs-window
|
||||
v-model="tab"
|
||||
class="config-tabs-window"
|
||||
:style="readonly ? 'pointer-events: none; opacity: 0.6;' : ''"
|
||||
>
|
||||
<v-tabs-window-item
|
||||
v-for="section in visibleSections"
|
||||
:key="section.key"
|
||||
:value="section.key"
|
||||
>
|
||||
<v-container fluid>
|
||||
<div
|
||||
v-for="(val2, key2, index2) in section.value['metadata']"
|
||||
:key="key2"
|
||||
>
|
||||
<!-- Support both traditional and JSON selector metadata -->
|
||||
<AstrBotConfigV4
|
||||
:metadata="{ [key2]: section.value['metadata'][key2] }"
|
||||
:iterable="config_data"
|
||||
:metadata-key="key2"
|
||||
:search-keyword="searchKeyword"
|
||||
/>
|
||||
</div>
|
||||
</v-container>
|
||||
</v-tabs-window-item>
|
||||
|
||||
|
||||
<div style="margin-left: 16px; padding-bottom: 16px">
|
||||
<small>{{ tm('help.helpPrefix') }}
|
||||
<a href="https://astrbot.app/" target="_blank">{{ tm('help.documentation') }}</a>
|
||||
{{ tm('help.helpMiddle') }}
|
||||
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=EYGsuUTfe00_iOu9JTXS7_TEpMkXOvwv&jump_from=webapi&authKey=uUEMKCROfsseS+8IzqPjzV3y1tzy4AkykwTib2jNkOFdzezF9s9XknqnIaf3CDft"
|
||||
target="_blank">{{ tm('help.support') }}</a>{{ tm('help.helpSuffix') }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</v-tabs-window>
|
||||
</div>
|
||||
<v-container v-if="visibleSections.length === 0" fluid class="px-0">
|
||||
<v-alert type="info" variant="tonal">
|
||||
{{ tm('search.noResult') }}
|
||||
</v-alert>
|
||||
</v-container>
|
||||
<div style="margin-left: 16px; padding-bottom: 16px">
|
||||
<small>{{ tm('help.helpPrefix') }}
|
||||
<a
|
||||
href="https://astrbot.app/"
|
||||
target="_blank"
|
||||
>{{ tm('help.documentation') }}</a>
|
||||
{{ tm('help.helpMiddle') }}
|
||||
<a
|
||||
href="https://qm.qq.com/cgi-bin/qm/qr?k=EYGsuUTfe00_iOu9JTXS7_TEpMkXOvwv&jump_from=webapi&authKey=uUEMKCROfsseS+8IzqPjzV3y1tzy4AkykwTib2jNkOFdzezF9s9XknqnIaf3CDft"
|
||||
target="_blank"
|
||||
>{{ tm('help.support') }}</a>{{ tm('help.helpSuffix') }}
|
||||
</small>
|
||||
</div>
|
||||
</v-tabs-window>
|
||||
</div>
|
||||
<v-container
|
||||
v-if="visibleSections.length === 0"
|
||||
fluid
|
||||
class="px-0"
|
||||
>
|
||||
<v-alert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ tm('search.noResult') }}
|
||||
</v-alert>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
<template>
|
||||
<v-dialog v-model="isOpen" max-width="480" persistent>
|
||||
<v-dialog
|
||||
v-model="isOpen"
|
||||
max-width="480"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="dialog-title d-flex align-center justify-space-between">
|
||||
<span>{{ title }}</span>
|
||||
<v-btn icon="mdi-close" variant="text" @click="handleClose"></v-btn>
|
||||
<v-btn
|
||||
icon="mdi-close"
|
||||
variant="text"
|
||||
@click="handleClose"
|
||||
/>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<div class="message-text">{{ message }}</div>
|
||||
<div class="message-text">
|
||||
{{ message }}
|
||||
</div>
|
||||
<div class="action-hints">
|
||||
<span class="hint-item">{{ confirmHint }}</span>
|
||||
<span class="hint-item">{{ cancelHint }}</span>
|
||||
@@ -14,9 +24,20 @@
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="gray" @click="handleCancel">{{ t('core.common.dialog.cancelButton') }}</v-btn>
|
||||
<v-btn color="red" @click="handleConfirm" class="confirm-button">{{ t('core.common.dialog.confirmButton') }}</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="gray"
|
||||
@click="handleCancel"
|
||||
>
|
||||
{{ t('core.common.dialog.cancelButton') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="red"
|
||||
class="confirm-button"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
{{ t('core.common.dialog.confirmButton') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
@@ -81,7 +81,7 @@ const handleInstall = (plugin) => {
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -108,23 +108,25 @@ const handleInstall = (plugin) => {
|
||||
plugin.display_name?.length
|
||||
? plugin.display_name
|
||||
: showPluginFullName
|
||||
? plugin.name
|
||||
: plugin.trimmedName
|
||||
? plugin.name
|
||||
: plugin.trimmedName
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center" style="gap: 4px; margin-bottom: 6px">
|
||||
<div
|
||||
class="d-flex align-center"
|
||||
style="gap: 4px; margin-bottom: 6px"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-account"
|
||||
size="x-small"
|
||||
style="color: rgba(var(--v-theme-on-surface), 0.5)"
|
||||
></v-icon>
|
||||
/>
|
||||
<a
|
||||
v-if="plugin?.social_link"
|
||||
:href="plugin.social_link"
|
||||
target="_blank"
|
||||
@click.stop
|
||||
class="text-subtitle-2 font-weight-medium"
|
||||
style="
|
||||
text-decoration: none;
|
||||
@@ -133,6 +135,7 @@ const handleInstall = (plugin) => {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
"
|
||||
@click.stop
|
||||
>
|
||||
{{ plugin.author }}
|
||||
</a>
|
||||
@@ -156,7 +159,7 @@ const handleInstall = (plugin) => {
|
||||
icon="mdi-source-branch"
|
||||
size="x-small"
|
||||
style="margin-right: 2px"
|
||||
></v-icon>
|
||||
/>
|
||||
<span>{{ plugin.version }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -186,7 +189,10 @@ const handleInstall = (plugin) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center" style="gap: 8px; margin-top: auto">
|
||||
<div
|
||||
class="d-flex align-center"
|
||||
style="gap: 8px; margin-top: auto"
|
||||
>
|
||||
<div
|
||||
v-if="plugin.stars !== undefined"
|
||||
class="d-flex align-center text-subtitle-2"
|
||||
@@ -196,7 +202,7 @@ const handleInstall = (plugin) => {
|
||||
icon="mdi-star"
|
||||
size="x-small"
|
||||
style="margin-right: 2px"
|
||||
></v-icon>
|
||||
/>
|
||||
<span>{{ plugin.stars }}</span>
|
||||
</div>
|
||||
<div
|
||||
@@ -208,7 +214,7 @@ const handleInstall = (plugin) => {
|
||||
icon="mdi-clock-outline"
|
||||
size="x-small"
|
||||
style="margin-right: 2px"
|
||||
></v-icon>
|
||||
/>
|
||||
<span>{{ new Date(plugin.updated_at).toLocaleString() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -229,8 +235,12 @@ const handleInstall = (plugin) => {
|
||||
>
|
||||
{{ tag === "danger" ? tm("tags.danger") : tag }}
|
||||
</v-chip>
|
||||
<v-menu v-if="plugin.tags && plugin.tags.length > 2" open-on-hover offset-y>
|
||||
<template v-slot:activator="{ props: menuProps }">
|
||||
<v-menu
|
||||
v-if="plugin.tags && plugin.tags.length > 2"
|
||||
open-on-hover
|
||||
offset-y
|
||||
>
|
||||
<template #activator="{ props: menuProps }">
|
||||
<v-chip
|
||||
v-bind="menuProps"
|
||||
color="grey"
|
||||
@@ -242,14 +252,21 @@ const handleInstall = (plugin) => {
|
||||
</v-chip>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item v-for="tag in plugin.tags.slice(2)" :key="tag">
|
||||
<v-chip :color="tag === 'danger' ? 'error' : 'primary'" label size="small">
|
||||
<v-list-item
|
||||
v-for="tag in plugin.tags.slice(2)"
|
||||
:key="tag"
|
||||
>
|
||||
<v-chip
|
||||
:color="tag === 'danger' ? 'error' : 'primary'"
|
||||
label
|
||||
size="small"
|
||||
>
|
||||
{{ tag === "danger" ? tm("tags.danger") : tag }}
|
||||
</v-chip>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="plugin?.repo"
|
||||
color="secondary"
|
||||
@@ -260,21 +277,31 @@ const handleInstall = (plugin) => {
|
||||
target="_blank"
|
||||
style="height: 32px"
|
||||
>
|
||||
<v-icon icon="mdi-github" start size="small"></v-icon>
|
||||
<v-icon
|
||||
icon="mdi-github"
|
||||
start
|
||||
size="small"
|
||||
/>
|
||||
{{ tm("buttons.viewRepo") }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="!plugin?.installed"
|
||||
color="primary"
|
||||
size="small"
|
||||
@click="handleInstall(plugin)"
|
||||
variant="flat"
|
||||
class="market-action-btn"
|
||||
style="height: 32px"
|
||||
@click="handleInstall(plugin)"
|
||||
>
|
||||
{{ tm("buttons.install") }}
|
||||
</v-btn>
|
||||
<v-chip v-else color="success" size="x-small" label style="height: 20px">
|
||||
<v-chip
|
||||
v-else
|
||||
color="success"
|
||||
size="x-small"
|
||||
label
|
||||
style="height: 20px"
|
||||
>
|
||||
✓ {{ tm("status.installed") }}
|
||||
</v-chip>
|
||||
</v-card-actions>
|
||||
|
||||
@@ -1,62 +1,130 @@
|
||||
<template>
|
||||
<div class="tools-page">
|
||||
<v-container fluid class="pa-0" elevation="0">
|
||||
<v-container
|
||||
fluid
|
||||
class="pa-0"
|
||||
elevation="0"
|
||||
>
|
||||
<!-- 页面标题 -->
|
||||
<v-row class="d-flex justify-space-between align-center px-4 py-3 pb-8">
|
||||
<div>
|
||||
<v-btn color="success" prepend-icon="mdi-plus" class="me-2" variant="tonal"
|
||||
@click="showMcpServerDialog = true" >
|
||||
<v-btn
|
||||
color="success"
|
||||
prepend-icon="mdi-plus"
|
||||
class="me-2"
|
||||
variant="tonal"
|
||||
@click="showMcpServerDialog = true"
|
||||
>
|
||||
{{ tm('mcpServers.buttons.add') }}
|
||||
</v-btn>
|
||||
<v-btn color="success" prepend-icon="mdi-refresh" variant="tonal" @click="showSyncMcpServerDialog = true"
|
||||
>
|
||||
<v-btn
|
||||
color="success"
|
||||
prepend-icon="mdi-refresh"
|
||||
variant="tonal"
|
||||
@click="showSyncMcpServerDialog = true"
|
||||
>
|
||||
{{ tm('mcpServers.buttons.sync') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-row>
|
||||
|
||||
<!-- MCP 服务器部分 -->
|
||||
<div v-if="mcpServers.length === 0" class="text-center pa-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-server-off</v-icon>
|
||||
<p class="text-grey mt-4">{{ tm('mcpServers.empty') }}</p>
|
||||
<div
|
||||
v-if="mcpServers.length === 0"
|
||||
class="text-center pa-8"
|
||||
>
|
||||
<v-icon
|
||||
size="64"
|
||||
color="grey-lighten-1"
|
||||
>
|
||||
mdi-server-off
|
||||
</v-icon>
|
||||
<p class="text-grey mt-4">
|
||||
{{ tm('mcpServers.empty') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<v-row v-else>
|
||||
<v-col v-for="(server, index) in mcpServers || []" :key="index" cols="12" md="6" lg="4" xl="3">
|
||||
<item-card style="background-color: rgb(var(--v-theme-mcpCardBg));" :item="server" title-field="name"
|
||||
enabled-field="active" @toggle-enabled="updateServerStatus" @delete="deleteServer" @edit="editServer">
|
||||
<template v-slot:item-details="{ item }">
|
||||
<v-col
|
||||
v-for="(server, index) in mcpServers || []"
|
||||
:key="index"
|
||||
cols="12"
|
||||
md="6"
|
||||
lg="4"
|
||||
xl="3"
|
||||
>
|
||||
<item-card
|
||||
style="background-color: rgb(var(--v-theme-mcpCardBg));"
|
||||
:item="server"
|
||||
title-field="name"
|
||||
enabled-field="active"
|
||||
@toggle-enabled="updateServerStatus"
|
||||
@delete="deleteServer"
|
||||
@edit="editServer"
|
||||
>
|
||||
<template #item-details="{ item }">
|
||||
<div class="d-flex align-center mb-2">
|
||||
<v-icon size="small" color="grey" class="me-2">mdi-file-code</v-icon>
|
||||
<span class="text-caption text-medium-emphasis text-truncate" :title="getServerConfigSummary(item)">
|
||||
<v-icon
|
||||
size="small"
|
||||
color="grey"
|
||||
class="me-2"
|
||||
>
|
||||
mdi-file-code
|
||||
</v-icon>
|
||||
<span
|
||||
class="text-caption text-medium-emphasis text-truncate"
|
||||
:title="getServerConfigSummary(item)"
|
||||
>
|
||||
{{ getServerConfigSummary(item) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex" style="gap: 8px;">
|
||||
<div
|
||||
class="d-flex"
|
||||
style="gap: 8px;"
|
||||
>
|
||||
<div>
|
||||
<div v-if="item.tools && item.tools.length > 0">
|
||||
<div class="d-flex align-center mb-1">
|
||||
<v-icon size="small" color="grey" class="me-2">mdi-tools</v-icon>
|
||||
<v-icon
|
||||
size="small"
|
||||
color="grey"
|
||||
class="me-2"
|
||||
>
|
||||
mdi-tools
|
||||
</v-icon>
|
||||
<v-dialog max-width="600px">
|
||||
<template v-slot:activator="{ props: listToolsProps }">
|
||||
<span class="text-caption text-medium-emphasis cursor-pointer" v-bind="listToolsProps"
|
||||
style="text-decoration: underline;">
|
||||
<template #activator="{ props: listToolsProps }">
|
||||
<span
|
||||
class="text-caption text-medium-emphasis cursor-pointer"
|
||||
v-bind="listToolsProps"
|
||||
style="text-decoration: underline;"
|
||||
>
|
||||
{{ tm('mcpServers.status.availableTools', { count: item.tools.length }) }} ({{ item.tools.length }})
|
||||
</span>
|
||||
</template>
|
||||
<template v-slot:default="{ isActive }">
|
||||
<template #default="{ isActive }">
|
||||
<v-card style="padding: 16px;">
|
||||
<v-card-title class="d-flex align-center">
|
||||
<span>{{ tm('mcpServers.status.availableTools') }}</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<ul>
|
||||
<li v-for="(tool, idx) in item.tools" :key="idx" style="margin: 8px 0px;">{{ tool }}</li>
|
||||
<li
|
||||
v-for="(tool, idx) in item.tools"
|
||||
:key="idx"
|
||||
style="margin: 8px 0px;"
|
||||
>
|
||||
{{ tool }}
|
||||
</li>
|
||||
</ul>
|
||||
</v-card-text>
|
||||
<v-card-actions class="d-flex justify-end">
|
||||
<v-btn variant="text" color="primary" @click="isActive.value = false">
|
||||
<v-btn
|
||||
variant="text"
|
||||
color="primary"
|
||||
@click="isActive.value = false"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
@@ -65,13 +133,29 @@
|
||||
</v-dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-caption text-medium-emphasis">
|
||||
<v-icon size="small" color="warning" class="me-1">mdi-alert-circle</v-icon>
|
||||
<div
|
||||
v-else
|
||||
class="text-caption text-medium-emphasis"
|
||||
>
|
||||
<v-icon
|
||||
size="small"
|
||||
color="warning"
|
||||
class="me-1"
|
||||
>
|
||||
mdi-alert-circle
|
||||
</v-icon>
|
||||
{{ tm('mcpServers.status.noTools') }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="mcpServerUpdateLoaders[item.name]" class="text-caption text-medium-emphasis">
|
||||
<v-progress-circular indeterminate color="primary" size="16"></v-progress-circular>
|
||||
<div
|
||||
v-if="mcpServerUpdateLoaders[item.name]"
|
||||
class="text-caption text-medium-emphasis"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
size="16"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -81,69 +165,129 @@
|
||||
</v-container>
|
||||
|
||||
<!-- 添加/编辑 MCP 服务器对话框 -->
|
||||
<v-dialog v-model="showMcpServerDialog" max-width="750px">
|
||||
<v-dialog
|
||||
v-model="showMcpServerDialog"
|
||||
max-width="750px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="pa-4 pl-6">
|
||||
<v-icon class="me-2">{{ isEditMode ? 'mdi-pencil' : 'mdi-plus' }}</v-icon>
|
||||
<v-icon class="me-2">
|
||||
{{ isEditMode ? 'mdi-pencil' : 'mdi-plus' }}
|
||||
</v-icon>
|
||||
<span>{{ isEditMode ? tm('dialogs.addServer.editTitle') : tm('dialogs.addServer.title') }}</span>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="py-4">
|
||||
<v-form @submit.prevent="saveServer" ref="form">
|
||||
<v-text-field v-model="currentServer.name" :label="tm('dialogs.addServer.fields.name')" variant="outlined"
|
||||
:rules="[v => !!v || tm('dialogs.addServer.fields.nameRequired')]" required class="mb-3"></v-text-field>
|
||||
<v-form
|
||||
ref="form"
|
||||
@submit.prevent="saveServer"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="currentServer.name"
|
||||
:label="tm('dialogs.addServer.fields.name')"
|
||||
variant="outlined"
|
||||
:rules="[v => !!v || tm('dialogs.addServer.fields.nameRequired')]"
|
||||
required
|
||||
class="mb-3"
|
||||
/>
|
||||
|
||||
<div class="mb-2 d-flex align-center">
|
||||
<span class="text-subtitle-1">{{ tm('dialogs.addServer.fields.config') }}</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn size="small" color="primary" variant="tonal" @click="setConfigTemplate('stdio')" class="me-1">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
class="me-1"
|
||||
@click="setConfigTemplate('stdio')"
|
||||
>
|
||||
{{ tm('mcpServers.buttons.useTemplateStdio') }}
|
||||
</v-btn>
|
||||
<v-btn size="small" color="primary" variant="tonal" @click="setConfigTemplate('streamable_http')"
|
||||
class="me-1">
|
||||
<v-btn
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
class="me-1"
|
||||
@click="setConfigTemplate('streamable_http')"
|
||||
>
|
||||
{{ tm('mcpServers.buttons.useTemplateStreamableHttp') }}
|
||||
</v-btn>
|
||||
<v-btn size="small" color="primary" variant="tonal" @click="setConfigTemplate('sse')" class="me-1">
|
||||
<v-btn
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
class="me-1"
|
||||
@click="setConfigTemplate('sse')"
|
||||
>
|
||||
{{ tm('mcpServers.buttons.useTemplateSse') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<small style="color: grey">*{{ tm('dialogs.addServer.tips.timeoutConfig') }}</small>
|
||||
|
||||
<div class="monaco-container" style="margin-top: 16px;">
|
||||
<VueMonacoEditor v-model:value="serverConfigJson" theme="vs-dark" language="json" :options="{
|
||||
minimap: {
|
||||
enabled: false
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
automaticLayout: true,
|
||||
lineNumbers: 'on',
|
||||
roundedSelection: true,
|
||||
tabSize: 2
|
||||
}" @change="validateJson" />
|
||||
<div
|
||||
class="monaco-container"
|
||||
style="margin-top: 16px;"
|
||||
>
|
||||
<VueMonacoEditor
|
||||
v-model:value="serverConfigJson"
|
||||
theme="vs-dark"
|
||||
language="json"
|
||||
:options="{
|
||||
minimap: {
|
||||
enabled: false
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
automaticLayout: true,
|
||||
lineNumbers: 'on',
|
||||
roundedSelection: true,
|
||||
tabSize: 2
|
||||
}"
|
||||
@change="validateJson"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="jsonError" class="mt-2 text-error">
|
||||
<v-icon color="error" size="small" class="me-1">mdi-alert-circle</v-icon>
|
||||
<div
|
||||
v-if="jsonError"
|
||||
class="mt-2 text-error"
|
||||
>
|
||||
<v-icon
|
||||
color="error"
|
||||
size="small"
|
||||
class="me-1"
|
||||
>
|
||||
mdi-alert-circle
|
||||
</v-icon>
|
||||
<span>{{ jsonError }}</span>
|
||||
</div>
|
||||
|
||||
</v-form>
|
||||
<div style="margin-top: 8px;">
|
||||
<small>{{ addServerDialogMessage }}</small>
|
||||
</div>
|
||||
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="closeServerDialog" :disabled="loading">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
:disabled="loading"
|
||||
@click="closeServerDialog"
|
||||
>
|
||||
{{ tm('dialogs.addServer.buttons.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn variant="text" @click="testServerConnection" :disabled="loading">
|
||||
<v-btn
|
||||
variant="text"
|
||||
:disabled="loading"
|
||||
@click="testServerConnection"
|
||||
>
|
||||
{{ tm('dialogs.addServer.buttons.testConnection') }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" @click="saveServer" :loading="loading" :disabled="!isServerFormValid">
|
||||
<v-btn
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
:disabled="!isServerFormValid"
|
||||
@click="saveServer"
|
||||
>
|
||||
{{ tm('dialogs.addServer.buttons.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
@@ -151,43 +295,82 @@
|
||||
</v-dialog>
|
||||
|
||||
<!-- 同步 MCP 服务器对话框 -->
|
||||
<v-dialog v-model="showSyncMcpServerDialog" max-width="500px" persistent>
|
||||
<v-dialog
|
||||
v-model="showSyncMcpServerDialog"
|
||||
max-width="500px"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="bg-primary text-white py-3">
|
||||
<span>同步外部平台 MCP 服务器</span>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="py-4">
|
||||
<v-select v-model="selectedMcpServerProvider" :items="mcpServerProviderList"
|
||||
label="选择平台" variant="outlined" required></v-select>
|
||||
<v-select
|
||||
v-model="selectedMcpServerProvider"
|
||||
:items="mcpServerProviderList"
|
||||
label="选择平台"
|
||||
variant="outlined"
|
||||
required
|
||||
/>
|
||||
<div v-if="selectedMcpServerProvider === 'modelscope'">
|
||||
<v-timeline align="start" side="end">
|
||||
<v-timeline-item icon="mdi-numeric-1" icon-color="rgb(var(--v-theme-background))">
|
||||
<v-timeline
|
||||
align="start"
|
||||
side="end"
|
||||
>
|
||||
<v-timeline-item
|
||||
icon="mdi-numeric-1"
|
||||
icon-color="rgb(var(--v-theme-background))"
|
||||
>
|
||||
<div>
|
||||
<div class="text-h4">发现 MCP 服务器</div>
|
||||
<div class="text-h4">
|
||||
发现 MCP 服务器
|
||||
</div>
|
||||
<p class="mt-2">
|
||||
访问 <a href="https://www.modelscope.cn/mcp" target="_blank">ModelScope 平台</a> 浏览需要的 MCP 服务器。
|
||||
访问 <a
|
||||
href="https://www.modelscope.cn/mcp"
|
||||
target="_blank"
|
||||
>ModelScope 平台</a> 浏览需要的 MCP 服务器。
|
||||
</p>
|
||||
</div>
|
||||
</v-timeline-item>
|
||||
|
||||
<v-timeline-item icon="mdi-numeric-2" icon-color="rgb(var(--v-theme-background))">
|
||||
<v-timeline-item
|
||||
icon="mdi-numeric-2"
|
||||
icon-color="rgb(var(--v-theme-background))"
|
||||
>
|
||||
<div>
|
||||
<div class="text-h4">获取访问令牌</div>
|
||||
<div class="text-h4">
|
||||
获取访问令牌
|
||||
</div>
|
||||
<p class="mt-2">
|
||||
从<a href="https://modelscope.cn/my/myaccesstoken" target="_blank">账户设置</a>中获取个人访问令牌。
|
||||
从<a
|
||||
href="https://modelscope.cn/my/myaccesstoken"
|
||||
target="_blank"
|
||||
>账户设置</a>中获取个人访问令牌。
|
||||
</p>
|
||||
</div>
|
||||
</v-timeline-item>
|
||||
|
||||
<v-timeline-item icon="mdi-numeric-3" icon-color="rgb(var(--v-theme-background))">
|
||||
<v-timeline-item
|
||||
icon="mdi-numeric-3"
|
||||
icon-color="rgb(var(--v-theme-background))"
|
||||
>
|
||||
<div>
|
||||
<div class="text-h4">输入您的访问令牌</div>
|
||||
<div class="text-h4">
|
||||
输入您的访问令牌
|
||||
</div>
|
||||
<p class="mt-2">
|
||||
输入您的访问令牌以同步 MCP 服务器。
|
||||
</p>
|
||||
<v-text-field v-model="mcpProviderToken" type="password" variant="outlined"
|
||||
label="访问令牌" class="mt-2" hide-details/>
|
||||
<v-text-field
|
||||
v-model="mcpProviderToken"
|
||||
type="password"
|
||||
variant="outlined"
|
||||
label="访问令牌"
|
||||
class="mt-2"
|
||||
hide-details
|
||||
/>
|
||||
</div>
|
||||
</v-timeline-item>
|
||||
</v-timeline>
|
||||
@@ -195,11 +378,20 @@
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="showSyncMcpServerDialog = false" :disabled="loading">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
:disabled="loading"
|
||||
@click="showSyncMcpServerDialog = false"
|
||||
>
|
||||
{{ tm('dialogs.addServer.buttons.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" @click="syncMcpServers" :loading="loading" :disabled="loading">
|
||||
<v-btn
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
:disabled="loading"
|
||||
@click="syncMcpServers"
|
||||
>
|
||||
{{ tm('dialogs.addServer.buttons.sync') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
@@ -207,7 +399,13 @@
|
||||
</v-dialog>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar :timeout="3000" elevation="24" :color="save_message_success" v-model="save_message_snack" location="top">
|
||||
<v-snackbar
|
||||
v-model="save_message_snack"
|
||||
:timeout="3000"
|
||||
elevation="24"
|
||||
:color="save_message_success"
|
||||
location="top"
|
||||
>
|
||||
{{ save_message }}
|
||||
</v-snackbar>
|
||||
</div>
|
||||
|
||||
@@ -54,7 +54,9 @@ const toggleOrder = () => {
|
||||
@update:model-value="updateSortBy"
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<v-icon size="small">mdi-sort</v-icon>
|
||||
<v-icon size="small">
|
||||
mdi-sort
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-select>
|
||||
|
||||
@@ -65,10 +67,15 @@ const toggleOrder = () => {
|
||||
density="compact"
|
||||
@click="toggleOrder"
|
||||
>
|
||||
<v-icon>{{
|
||||
order === "desc" ? "mdi-arrow-down-thin" : "mdi-arrow-up-thin"
|
||||
}}</v-icon>
|
||||
<v-tooltip activator="parent" location="top">
|
||||
<v-icon>
|
||||
{{
|
||||
order === "desc" ? "mdi-arrow-down-thin" : "mdi-arrow-up-thin"
|
||||
}}
|
||||
</v-icon>
|
||||
<v-tooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
>
|
||||
{{ order === "desc" ? descendingLabel : ascendingLabel }}
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<template>
|
||||
<div class="skills-page">
|
||||
<v-container fluid class="pa-0" elevation="0">
|
||||
<v-container
|
||||
fluid
|
||||
class="pa-0"
|
||||
elevation="0"
|
||||
>
|
||||
<v-row class="d-flex justify-space-between align-center px-4 py-3 pb-4">
|
||||
<div>
|
||||
<v-btn
|
||||
@@ -22,15 +26,30 @@
|
||||
{{ tm("skills.refresh") }}
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-btn-toggle v-model="mode" mandatory divided density="comfortable">
|
||||
<v-btn value="local">{{ tm("skills.modeLocal") }}</v-btn>
|
||||
<v-btn value="neo" :disabled="!neoEnabled">{{
|
||||
tm("skills.modeNeo")
|
||||
}}</v-btn>
|
||||
<v-btn-toggle
|
||||
v-model="mode"
|
||||
mandatory
|
||||
divided
|
||||
density="comfortable"
|
||||
>
|
||||
<v-btn value="local">
|
||||
{{ tm("skills.modeLocal") }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
value="neo"
|
||||
:disabled="!neoEnabled"
|
||||
>
|
||||
{{
|
||||
tm("skills.modeNeo")
|
||||
}}
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-row>
|
||||
|
||||
<div v-if="mode === 'local'" class="px-2 pb-2 d-flex flex-column ga-2">
|
||||
<div
|
||||
v-if="mode === 'local'"
|
||||
class="px-2 pb-2 d-flex flex-column ga-2"
|
||||
>
|
||||
<small style="color: grey">{{ tm("skills.runtimeHint") }}</small>
|
||||
<v-alert
|
||||
v-if="runtime === 'sandbox' && !sandboxCache.ready"
|
||||
@@ -43,7 +62,10 @@
|
||||
</v-alert>
|
||||
</div>
|
||||
|
||||
<div v-if="mode === 'neo' && !neoEnabled" class="px-3 pb-3">
|
||||
<div
|
||||
v-if="mode === 'neo' && !neoEnabled"
|
||||
class="px-3 pb-3"
|
||||
>
|
||||
<v-alert
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
@@ -59,15 +81,28 @@
|
||||
v-if="loading"
|
||||
indeterminate
|
||||
color="primary"
|
||||
></v-progress-linear>
|
||||
/>
|
||||
|
||||
<div v-else-if="skills.length === 0" class="text-center pa-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-folder-open</v-icon>
|
||||
<p class="text-grey mt-4">{{ tm("skills.empty") }}</p>
|
||||
<div
|
||||
v-else-if="skills.length === 0"
|
||||
class="text-center pa-8"
|
||||
>
|
||||
<v-icon
|
||||
size="64"
|
||||
color="grey-lighten-1"
|
||||
>
|
||||
mdi-folder-open
|
||||
</v-icon>
|
||||
<p class="text-grey mt-4">
|
||||
{{ tm("skills.empty") }}
|
||||
</p>
|
||||
<small class="text-grey">{{ tm("skills.emptyHint") }}</small>
|
||||
</div>
|
||||
|
||||
<v-row v-else align="stretch">
|
||||
<v-row
|
||||
v-else
|
||||
align="stretch"
|
||||
>
|
||||
<v-col
|
||||
v-for="skill in skills"
|
||||
:key="skill.name"
|
||||
@@ -100,12 +135,22 @@
|
||||
<div
|
||||
class="text-caption text-medium-emphasis skill-description"
|
||||
>
|
||||
<v-icon size="small" class="me-1">mdi-text</v-icon>
|
||||
<v-icon
|
||||
size="small"
|
||||
class="me-1"
|
||||
>
|
||||
mdi-text
|
||||
</v-icon>
|
||||
{{ item.description || tm("skills.noDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-caption text-medium-emphasis skill-path">
|
||||
<v-icon size="small" class="me-1">mdi-file-document</v-icon>
|
||||
<v-icon
|
||||
size="small"
|
||||
class="me-1"
|
||||
>
|
||||
mdi-file-document
|
||||
</v-icon>
|
||||
{{ tm("skills.path") }}: {{ item.path }}
|
||||
</div>
|
||||
</template>
|
||||
@@ -117,8 +162,8 @@
|
||||
rounded="xl"
|
||||
:disabled="
|
||||
itemLoading[item.name] ||
|
||||
false ||
|
||||
isSandboxPresetSkill(item)
|
||||
false ||
|
||||
isSandboxPresetSkill(item)
|
||||
"
|
||||
@click="downloadSkill(item)"
|
||||
>
|
||||
@@ -131,12 +176,17 @@
|
||||
</template>
|
||||
|
||||
<template v-else-if="mode === 'neo' && neoEnabled">
|
||||
<v-card class="mx-3 mb-4 pa-4 neo-filter-card" variant="outlined">
|
||||
<v-card
|
||||
class="mx-3 mb-4 pa-4 neo-filter-card"
|
||||
variant="outlined"
|
||||
>
|
||||
<div
|
||||
class="d-flex flex-wrap justify-space-between align-center ga-2 mb-3"
|
||||
>
|
||||
<div>
|
||||
<div class="text-subtitle-1 font-weight-bold">Neo Skills</div>
|
||||
<div class="text-subtitle-1 font-weight-bold">
|
||||
Neo Skills
|
||||
</div>
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
{{ tm("skills.neoFilterHint") }}
|
||||
</div>
|
||||
@@ -152,7 +202,10 @@
|
||||
</div>
|
||||
|
||||
<v-row class="ga-md-0 ga-2">
|
||||
<v-col cols="12" md="4">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="neoFilters.skill_key"
|
||||
:label="tm('skills.neoSkillKey')"
|
||||
@@ -162,7 +215,10 @@
|
||||
variant="outlined"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<v-select
|
||||
v-model="neoFilters.status"
|
||||
:label="tm('skills.neoStatus')"
|
||||
@@ -175,7 +231,10 @@
|
||||
variant="outlined"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
>
|
||||
<v-select
|
||||
v-model="neoFilters.stage"
|
||||
:label="tm('skills.neoStage')"
|
||||
@@ -195,24 +254,41 @@
|
||||
v-if="neoLoading"
|
||||
indeterminate
|
||||
color="primary"
|
||||
></v-progress-linear>
|
||||
/>
|
||||
|
||||
<div class="mx-3 mb-3 d-flex flex-wrap ga-2">
|
||||
<v-chip size="small" color="primary" variant="tonal"
|
||||
>Candidates: {{ neoCandidates.length }}</v-chip
|
||||
<v-chip
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
>
|
||||
<v-chip size="small" color="indigo" variant="tonal"
|
||||
>Releases: {{ neoReleases.length }}</v-chip
|
||||
Candidates: {{ neoCandidates.length }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
size="small"
|
||||
color="indigo"
|
||||
variant="tonal"
|
||||
>
|
||||
<v-chip size="small" color="success" variant="tonal"
|
||||
>Active: {{ activeReleaseCount }}</v-chip
|
||||
Releases: {{ neoReleases.length }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
size="small"
|
||||
color="success"
|
||||
variant="tonal"
|
||||
>
|
||||
Active: {{ activeReleaseCount }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<v-card class="mx-3 mb-4 neo-table-card" variant="outlined">
|
||||
<v-card-title class="text-subtitle-1 font-weight-bold">{{
|
||||
tm("skills.neoCandidates")
|
||||
}}</v-card-title>
|
||||
<v-card
|
||||
class="mx-3 mb-4 neo-table-card"
|
||||
variant="outlined"
|
||||
>
|
||||
<v-card-title class="text-subtitle-1 font-weight-bold">
|
||||
{{
|
||||
tm("skills.neoCandidates")
|
||||
}}
|
||||
</v-card-title>
|
||||
<v-data-table
|
||||
:headers="candidateHeaders"
|
||||
:items="neoCandidates"
|
||||
@@ -282,10 +358,15 @@
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
|
||||
<v-card class="mx-3 mb-4 neo-table-card" variant="outlined">
|
||||
<v-card-title class="text-subtitle-1 font-weight-bold">{{
|
||||
tm("skills.neoReleases")
|
||||
}}</v-card-title>
|
||||
<v-card
|
||||
class="mx-3 mb-4 neo-table-card"
|
||||
variant="outlined"
|
||||
>
|
||||
<v-card-title class="text-subtitle-1 font-weight-bold">
|
||||
{{
|
||||
tm("skills.neoReleases")
|
||||
}}
|
||||
</v-card-title>
|
||||
<v-data-table
|
||||
:headers="releaseHeaders"
|
||||
:items="neoReleases"
|
||||
@@ -339,7 +420,11 @@
|
||||
</template>
|
||||
</v-container>
|
||||
|
||||
<v-dialog v-model="uploadDialog" max-width="880px" :persistent="uploading">
|
||||
<v-dialog
|
||||
v-model="uploadDialog"
|
||||
max-width="880px"
|
||||
:persistent="uploading"
|
||||
>
|
||||
<v-card class="skills-upload-dialog">
|
||||
<v-card-title class="skills-upload-dialog__header px-6 pt-6 pb-2">
|
||||
<div class="skills-upload-dialog__heading">
|
||||
@@ -364,26 +449,34 @@
|
||||
</p>
|
||||
|
||||
<div class="skills-upload-structure-note">
|
||||
<v-icon size="18">mdi-information-outline</v-icon>
|
||||
<v-icon size="18">
|
||||
mdi-information-outline
|
||||
</v-icon>
|
||||
<span>{{ tm("skills.structureRequirement") }}</span>
|
||||
</div>
|
||||
|
||||
<div class="skills-upload-capabilities">
|
||||
<div class="skills-upload-capability">
|
||||
<div class="skills-upload-capability__icon">
|
||||
<v-icon size="18">mdi-layers-outline</v-icon>
|
||||
<v-icon size="18">
|
||||
mdi-layers-outline
|
||||
</v-icon>
|
||||
</div>
|
||||
<span>{{ tm("skills.abilityMultiple") }}</span>
|
||||
</div>
|
||||
<div class="skills-upload-capability">
|
||||
<div class="skills-upload-capability__icon">
|
||||
<v-icon size="18">mdi-shield-check-outline</v-icon>
|
||||
<v-icon size="18">
|
||||
mdi-shield-check-outline
|
||||
</v-icon>
|
||||
</div>
|
||||
<span>{{ tm("skills.abilityValidate") }}</span>
|
||||
</div>
|
||||
<div class="skills-upload-capability">
|
||||
<div class="skills-upload-capability__icon">
|
||||
<v-icon size="18">mdi-skip-next-circle-outline</v-icon>
|
||||
<v-icon size="18">
|
||||
mdi-skip-next-circle-outline
|
||||
</v-icon>
|
||||
</div>
|
||||
<span>{{ tm("skills.abilitySkip") }}</span>
|
||||
</div>
|
||||
@@ -403,7 +496,9 @@
|
||||
@drop.prevent="handleUploadDrop"
|
||||
>
|
||||
<div class="skills-dropzone__icon">
|
||||
<v-icon size="34">mdi-folder-zip-outline</v-icon>
|
||||
<v-icon size="34">
|
||||
mdi-folder-zip-outline
|
||||
</v-icon>
|
||||
</div>
|
||||
<div class="text-h6 font-weight-medium">
|
||||
{{ tm("skills.dropzoneTitle") }}
|
||||
@@ -421,10 +516,13 @@
|
||||
hidden
|
||||
accept=".zip"
|
||||
@change="handleUploadSelection"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
|
||||
<div v-if="uploadItems.length > 0" class="skills-upload-summary">
|
||||
<div
|
||||
v-if="uploadItems.length > 0"
|
||||
class="skills-upload-summary"
|
||||
>
|
||||
<v-chip
|
||||
size="small"
|
||||
variant="flat"
|
||||
@@ -479,7 +577,10 @@
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div v-if="uploadItems.length > 0" class="skills-upload-list">
|
||||
<div
|
||||
v-if="uploadItems.length > 0"
|
||||
class="skills-upload-list"
|
||||
>
|
||||
<div class="skills-upload-list__header">
|
||||
<span>{{ tm("skills.fileListTitle") }}</span>
|
||||
</div>
|
||||
@@ -489,7 +590,9 @@
|
||||
class="skills-upload-row"
|
||||
>
|
||||
<div class="skills-upload-row__meta">
|
||||
<div class="skills-upload-row__name">{{ item.name }}</div>
|
||||
<div class="skills-upload-row__name">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<div class="skills-upload-row__size">
|
||||
{{ formatFileSize(item.size) }}
|
||||
</div>
|
||||
@@ -515,7 +618,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="skills-upload-empty">
|
||||
<div
|
||||
v-else
|
||||
class="skills-upload-empty"
|
||||
>
|
||||
{{ tm("skills.fileListEmpty") }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
@@ -546,31 +652,51 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog v-model="deleteDialog" max-width="400px">
|
||||
<v-dialog
|
||||
v-model="deleteDialog"
|
||||
max-width="400px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>{{ tm("skills.deleteTitle") }}</v-card-title>
|
||||
<v-card-text>{{ tm("skills.deleteMessage") }}</v-card-text>
|
||||
<v-card-actions class="d-flex justify-end">
|
||||
<v-btn variant="text" @click="deleteDialog = false">{{
|
||||
tm("skills.cancel")
|
||||
}}</v-btn>
|
||||
<v-btn color="error" :loading="deleting" @click="deleteSkill">
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="deleteDialog = false"
|
||||
>
|
||||
{{
|
||||
tm("skills.cancel")
|
||||
}}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
:loading="deleting"
|
||||
@click="deleteSkill"
|
||||
>
|
||||
{{ t("core.common.itemCard.delete") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog v-model="payloadDialog.show" max-width="820px">
|
||||
<v-dialog
|
||||
v-model="payloadDialog.show"
|
||||
max-width="820px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>{{ tm("skills.neoPayloadTitle") }}</v-card-title>
|
||||
<v-card-text>
|
||||
<pre class="payload-preview">{{ payloadDialog.content }}</pre>
|
||||
</v-card-text>
|
||||
<v-card-actions class="d-flex justify-end">
|
||||
<v-btn variant="text" @click="payloadDialog.show = false">{{
|
||||
tm("skills.cancel")
|
||||
}}</v-btn>
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="payloadDialog.show = false"
|
||||
>
|
||||
{{
|
||||
tm("skills.cancel")
|
||||
}}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
@@ -58,49 +58,68 @@ const statusItems = [
|
||||
|
||||
<template>
|
||||
<!-- 过滤器行 -->
|
||||
<v-row class="mb-4" align="center">
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-row
|
||||
class="mb-4"
|
||||
align="center"
|
||||
>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="3"
|
||||
>
|
||||
<v-select
|
||||
:model-value="pluginFilter"
|
||||
@update:model-value="emit('update:pluginFilter', $event)"
|
||||
:items="pluginItems"
|
||||
:label="tm('filters.byPlugin')"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
@update:model-value="emit('update:pluginFilter', $event)"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="2">
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="2"
|
||||
>
|
||||
<v-select
|
||||
:model-value="typeFilter"
|
||||
@update:model-value="emit('update:typeFilter', $event)"
|
||||
:items="typeItems"
|
||||
:label="tm('filters.byType')"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
@update:model-value="emit('update:typeFilter', $event)"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="2">
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="2"
|
||||
>
|
||||
<v-select
|
||||
:model-value="permissionFilter"
|
||||
@update:model-value="emit('update:permissionFilter', $event)"
|
||||
:items="permissionItems"
|
||||
:label="tm('filters.byPermission')"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
@update:model-value="emit('update:permissionFilter', $event)"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="2">
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="2"
|
||||
>
|
||||
<v-select
|
||||
:model-value="statusFilter"
|
||||
@update:model-value="emit('update:statusFilter', $event)"
|
||||
:items="statusItems"
|
||||
:label="tm('filters.byStatus')"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
@update:model-value="emit('update:statusFilter', $event)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -110,7 +129,6 @@ const statusItems = [
|
||||
<div style="min-width: 200px; max-width: 350px; flex: 1; border: 1px solid #B9B9B9; border-radius: 16px;">
|
||||
<v-text-field
|
||||
:model-value="searchQuery"
|
||||
@update:model-value="emit('update:searchQuery', normalizeTextInput($event))"
|
||||
density="compact"
|
||||
:label="tm('search.placeholder')"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
@@ -119,25 +137,40 @@ const statusItems = [
|
||||
flat
|
||||
hide-details
|
||||
single-line
|
||||
@update:model-value="emit('update:searchQuery', normalizeTextInput($event))"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex align-center ga-4">
|
||||
<slot name="stats"></slot>
|
||||
<v-divider vertical class="mx-1" style="height: 20px;" />
|
||||
<slot name="stats" />
|
||||
<v-divider
|
||||
vertical
|
||||
class="mx-1"
|
||||
style="height: 20px;"
|
||||
/>
|
||||
<v-checkbox
|
||||
:model-value="effectiveShowSystemPlugins"
|
||||
@update:model-value="emit('update:showSystemPlugins', !!$event)"
|
||||
:label="tm('filters.showSystemPlugins')"
|
||||
density="compact"
|
||||
hide-details
|
||||
:disabled="hasSystemPluginConflict"
|
||||
class="system-plugin-checkbox"
|
||||
@update:model-value="emit('update:showSystemPlugins', !!$event)"
|
||||
>
|
||||
<template v-slot:label>
|
||||
<template #label>
|
||||
<span class="text-body-2">{{ tm('filters.showSystemPlugins') }}</span>
|
||||
<v-tooltip v-if="hasSystemPluginConflict" location="top">
|
||||
<template v-slot:activator="{ props: tooltipProps }">
|
||||
<v-icon v-bind="tooltipProps" size="16" color="warning" class="ml-1">mdi-alert-circle</v-icon>
|
||||
<v-tooltip
|
||||
v-if="hasSystemPluginConflict"
|
||||
location="top"
|
||||
>
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-icon
|
||||
v-bind="tooltipProps"
|
||||
size="16"
|
||||
color="warning"
|
||||
class="ml-1"
|
||||
>
|
||||
mdi-alert-circle
|
||||
</v-icon>
|
||||
</template>
|
||||
{{ tm('filters.systemPluginConflictHint') }}
|
||||
</v-tooltip>
|
||||
|
||||
@@ -102,7 +102,7 @@ const getRowProps = ({ item }: { item: CommandItem }) => {
|
||||
:row-props="getRowProps"
|
||||
:loading="props.loading"
|
||||
>
|
||||
<template v-slot:item.effective_command="{ item }">
|
||||
<template #item.effective_command="{ item }">
|
||||
<div class="d-flex align-center py-2">
|
||||
<!-- 展开/折叠按钮(针对指令组) -->
|
||||
<v-btn
|
||||
@@ -113,10 +113,15 @@ const getRowProps = ({ item }: { item: CommandItem }) => {
|
||||
class="mr-1"
|
||||
@click.stop="emit('toggle-expand', item)"
|
||||
>
|
||||
<v-icon size="18">{{ isGroupExpanded(item) ? 'mdi-chevron-down' : 'mdi-chevron-right' }}</v-icon>
|
||||
<v-icon size="18">
|
||||
{{ isGroupExpanded(item) ? 'mdi-chevron-down' : 'mdi-chevron-right' }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
<!-- 子指令缩进 -->
|
||||
<div v-else-if="item.type === 'sub_command'" class="ml-6"></div>
|
||||
<div
|
||||
v-else-if="item.type === 'sub_command'"
|
||||
class="ml-6"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-subtitle-1 font-weight-medium">
|
||||
<code :class="{ 'sub-command-code': item.type === 'sub_command' }">{{ item.effective_command }}</code>
|
||||
@@ -125,30 +130,40 @@ const getRowProps = ({ item }: { item: CommandItem }) => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.type="{ item }">
|
||||
<template #item.type="{ item }">
|
||||
<v-chip
|
||||
:color="getTypeInfo(item.type).color"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
<v-icon start size="14">{{ getTypeInfo(item.type).icon }}</v-icon>
|
||||
<v-icon
|
||||
start
|
||||
size="14"
|
||||
>
|
||||
{{ getTypeInfo(item.type).icon }}
|
||||
</v-icon>
|
||||
{{ getTypeInfo(item.type).text }}{{ item.is_group && item.sub_commands?.length > 0 ? `(${item.sub_commands.length})` : '' }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.plugin="{ item }">
|
||||
<div class="text-body-2">{{ item.plugin_display_name || item.plugin }}</div>
|
||||
<template #item.plugin="{ item }">
|
||||
<div class="text-body-2">
|
||||
{{ item.plugin_display_name || item.plugin }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.description="{ item }">
|
||||
<div class="text-body-2 text-medium-emphasis" style="max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||||
<template #item.description="{ item }">
|
||||
<div
|
||||
class="text-body-2 text-medium-emphasis"
|
||||
style="max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
|
||||
>
|
||||
{{ item.description || '-' }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.permission="{ item }">
|
||||
<template #item.permission="{ item }">
|
||||
<v-menu location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<template #activator="{ props }">
|
||||
<v-chip
|
||||
v-bind="props"
|
||||
:color="getPermissionColor(item.permission)"
|
||||
@@ -157,21 +172,26 @@ const getRowProps = ({ item }: { item: CommandItem }) => {
|
||||
link
|
||||
>
|
||||
{{ getPermissionLabel(item.permission) }}
|
||||
<v-icon end size="14">mdi-chevron-down</v-icon>
|
||||
<v-icon
|
||||
end
|
||||
size="14"
|
||||
>
|
||||
mdi-chevron-down
|
||||
</v-icon>
|
||||
</v-chip>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item
|
||||
:value="'member'"
|
||||
@click="$emit('update-permission', item, 'member')"
|
||||
:active="item.permission !== 'admin'"
|
||||
@click="$emit('update-permission', item, 'member')"
|
||||
>
|
||||
<v-list-item-title>{{ tm('permission.everyone') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
:value="'admin'"
|
||||
@click="$emit('update-permission', item, 'admin')"
|
||||
:active="item.permission === 'admin'"
|
||||
@click="$emit('update-permission', item, 'admin')"
|
||||
>
|
||||
<v-list-item-title>{{ tm('permission.admin') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
@@ -179,7 +199,7 @@ const getRowProps = ({ item }: { item: CommandItem }) => {
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.enabled="{ item }">
|
||||
<template #item.enabled="{ item }">
|
||||
<v-chip
|
||||
:color="getStatusInfo(item).color"
|
||||
size="small"
|
||||
@@ -190,9 +210,13 @@ const getRowProps = ({ item }: { item: CommandItem }) => {
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<template #item.actions="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<v-btn-group density="default" variant="text" color="primary">
|
||||
<v-btn-group
|
||||
density="default"
|
||||
variant="text"
|
||||
color="primary"
|
||||
>
|
||||
<v-btn
|
||||
v-if="!item.enabled"
|
||||
icon
|
||||
@@ -200,8 +224,15 @@ const getRowProps = ({ item }: { item: CommandItem }) => {
|
||||
color="success"
|
||||
@click="emit('toggle-command', item)"
|
||||
>
|
||||
<v-icon size="22">mdi-play</v-icon>
|
||||
<v-tooltip activator="parent" location="top">{{ tm('tooltips.enable') }}</v-tooltip>
|
||||
<v-icon size="22">
|
||||
mdi-play
|
||||
</v-icon>
|
||||
<v-tooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
>
|
||||
{{ tm('tooltips.enable') }}
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
@@ -210,28 +241,68 @@ const getRowProps = ({ item }: { item: CommandItem }) => {
|
||||
color="error"
|
||||
@click="emit('toggle-command', item)"
|
||||
>
|
||||
<v-icon size="22">mdi-pause</v-icon>
|
||||
<v-tooltip activator="parent" location="top">{{ tm('tooltips.disable') }}</v-tooltip>
|
||||
<v-icon size="22">
|
||||
mdi-pause
|
||||
</v-icon>
|
||||
<v-tooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
>
|
||||
{{ tm('tooltips.disable') }}
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon size="small" color="warning" @click="emit('rename', item)">
|
||||
<v-icon size="22">mdi-pencil</v-icon>
|
||||
<v-tooltip activator="parent" location="top">{{ tm('tooltips.rename') }}</v-tooltip>
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
color="warning"
|
||||
@click="emit('rename', item)"
|
||||
>
|
||||
<v-icon size="22">
|
||||
mdi-pencil
|
||||
</v-icon>
|
||||
<v-tooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
>
|
||||
{{ tm('tooltips.rename') }}
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon size="small" @click="emit('view-details', item)">
|
||||
<v-icon size="22">mdi-information</v-icon>
|
||||
<v-tooltip activator="parent" location="top">{{ tm('tooltips.viewDetails') }}</v-tooltip>
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
@click="emit('view-details', item)"
|
||||
>
|
||||
<v-icon size="22">
|
||||
mdi-information
|
||||
</v-icon>
|
||||
<v-tooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
>
|
||||
{{ tm('tooltips.viewDetails') }}
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
</v-btn-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:no-data>
|
||||
<template #no-data>
|
||||
<div class="text-center pa-8">
|
||||
<v-icon size="64" color="info" class="mb-4">mdi-console-line</v-icon>
|
||||
<div class="text-h5 mb-2">{{ tm('empty.noCommands') }}</div>
|
||||
<div class="text-body-1 mb-4">{{ tm('empty.noCommandsDesc') }}</div>
|
||||
<v-icon
|
||||
size="64"
|
||||
color="info"
|
||||
class="mb-4"
|
||||
>
|
||||
mdi-console-line
|
||||
</v-icon>
|
||||
<div class="text-h5 mb-2">
|
||||
{{ tm('empty.noCommands') }}
|
||||
</div>
|
||||
<div class="text-body-1 mb-4">
|
||||
{{ tm('empty.noCommandsDesc') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
|
||||
@@ -46,54 +46,86 @@ const getPermissionLabel = (permission: string): string => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog :model-value="show" @update:model-value="emit('update:show', $event)" max-width="500">
|
||||
<v-dialog
|
||||
:model-value="show"
|
||||
max-width="500"
|
||||
@update:model-value="emit('update:show', $event)"
|
||||
>
|
||||
<v-card v-if="command">
|
||||
<v-card-title class="text-h5">{{ tm('dialogs.details.title') }}</v-card-title>
|
||||
<v-card-title class="text-h5">
|
||||
{{ tm('dialogs.details.title') }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-list density="compact">
|
||||
<v-list-item>
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.type') }}</v-list-item-title>
|
||||
<v-list-item-title class="font-weight-bold">
|
||||
{{ tm('dialogs.details.type') }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<v-chip
|
||||
:color="getTypeInfo(command.type).color"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
<v-icon start size="14">{{ getTypeInfo(command.type).icon }}</v-icon>
|
||||
<v-icon
|
||||
start
|
||||
size="14"
|
||||
>
|
||||
{{ getTypeInfo(command.type).icon }}
|
||||
</v-icon>
|
||||
{{ getTypeInfo(command.type).text }}
|
||||
</v-chip>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.handler') }}</v-list-item-title>
|
||||
<v-list-item-title class="font-weight-bold">
|
||||
{{ tm('dialogs.details.handler') }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle><code>{{ command.handler_name }}</code></v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.module') }}</v-list-item-title>
|
||||
<v-list-item-title class="font-weight-bold">
|
||||
{{ tm('dialogs.details.module') }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle><code>{{ command.module_path }}</code></v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.originalCommand') }}</v-list-item-title>
|
||||
<v-list-item-title class="font-weight-bold">
|
||||
{{ tm('dialogs.details.originalCommand') }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle><code>{{ command.original_command }}</code></v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.effectiveCommand') }}</v-list-item-title>
|
||||
<v-list-item-title class="font-weight-bold">
|
||||
{{ tm('dialogs.details.effectiveCommand') }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle><code>{{ command.effective_command }}</code></v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="command.parent_signature">
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.parentGroup') }}</v-list-item-title>
|
||||
<v-list-item-title class="font-weight-bold">
|
||||
{{ tm('dialogs.details.parentGroup') }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle><code>{{ command.parent_signature }}</code></v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="command.aliases.length > 0">
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.aliases') }}</v-list-item-title>
|
||||
<v-list-item-title class="font-weight-bold">
|
||||
{{ tm('dialogs.details.aliases') }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<v-chip v-for="alias in command.aliases" :key="alias" size="small" class="mr-1">
|
||||
<v-chip
|
||||
v-for="alias in command.aliases"
|
||||
:key="alias"
|
||||
size="small"
|
||||
class="mr-1"
|
||||
>
|
||||
{{ alias }}
|
||||
</v-chip>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="command.is_group && command.sub_commands?.length > 0">
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.subCommands') }}</v-list-item-title>
|
||||
<v-list-item-title class="font-weight-bold">
|
||||
{{ tm('dialogs.details.subCommands') }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<div class="d-flex flex-wrap ga-1 mt-1">
|
||||
<v-chip
|
||||
@@ -108,24 +140,40 @@ const getPermissionLabel = (permission: string): string => {
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.permission') }}</v-list-item-title>
|
||||
<v-list-item-title class="font-weight-bold">
|
||||
{{ tm('dialogs.details.permission') }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<v-chip :color="getPermissionColor(command.permission)" size="small">
|
||||
<v-chip
|
||||
:color="getPermissionColor(command.permission)"
|
||||
size="small"
|
||||
>
|
||||
{{ getPermissionLabel(command.permission) }}
|
||||
</v-chip>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="command.has_conflict">
|
||||
<v-list-item-title class="font-weight-bold">{{ tm('dialogs.details.conflictStatus') }}</v-list-item-title>
|
||||
<v-list-item-title class="font-weight-bold">
|
||||
{{ tm('dialogs.details.conflictStatus') }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<v-chip color="warning" size="small">{{ tm('status.conflict') }}</v-chip>
|
||||
<v-chip
|
||||
color="warning"
|
||||
size="small"
|
||||
>
|
||||
{{ tm('status.conflict') }}
|
||||
</v-chip>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="primary" variant="text" @click="emit('update:show', false)">
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
@click="emit('update:show', false)"
|
||||
>
|
||||
{{ t('core.actions.close') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
|
||||
@@ -57,21 +57,31 @@ watch(showAliasEditor, (open) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog :model-value="show" @update:model-value="emit('update:show', $event)" max-width="500">
|
||||
<v-dialog
|
||||
:model-value="show"
|
||||
max-width="500"
|
||||
@update:model-value="emit('update:show', $event)"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="text-h5">{{ tm('dialogs.rename.title') }}</v-card-title>
|
||||
<v-card-title class="text-h5">
|
||||
{{ tm('dialogs.rename.title') }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
:model-value="newName"
|
||||
@update:model-value="emit('update:newName', $event)"
|
||||
:label="tm('dialogs.rename.newName')"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
autofocus
|
||||
class="mb-2"
|
||||
@update:model-value="emit('update:newName', $event)"
|
||||
/>
|
||||
|
||||
<v-card variant="outlined" class="mt-2" elevation="0">
|
||||
<v-card
|
||||
variant="outlined"
|
||||
class="mt-2"
|
||||
elevation="0"
|
||||
>
|
||||
<div
|
||||
class="d-flex align-center justify-space-between px-4 py-3"
|
||||
role="button"
|
||||
@@ -80,22 +90,40 @@ watch(showAliasEditor, (open) => {
|
||||
@keydown.enter.prevent="showAliasEditor = !showAliasEditor"
|
||||
@keydown.space.prevent="showAliasEditor = !showAliasEditor"
|
||||
>
|
||||
<div class="text-subtitle-1">{{ tm('dialogs.rename.aliases') }}</div>
|
||||
<v-icon size="20">{{ showAliasEditor ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
|
||||
<div class="text-subtitle-1">
|
||||
{{ tm('dialogs.rename.aliases') }}
|
||||
</div>
|
||||
<v-icon size="20">
|
||||
{{ showAliasEditor ? 'mdi-chevron-up' : 'mdi-chevron-down' }}
|
||||
</v-icon>
|
||||
</div>
|
||||
<v-divider v-if="showAliasEditor" />
|
||||
<v-slide-y-transition>
|
||||
<div v-if="aliasEditorEverOpened" v-show="showAliasEditor" class="px-4 py-3">
|
||||
<div v-for="(alias, index) in aliases" :key="index" class="d-flex align-center mb-2">
|
||||
<div
|
||||
v-if="aliasEditorEverOpened"
|
||||
v-show="showAliasEditor"
|
||||
class="px-4 py-3"
|
||||
>
|
||||
<div
|
||||
v-for="(alias, index) in aliases"
|
||||
:key="index"
|
||||
class="d-flex align-center mb-2"
|
||||
>
|
||||
<v-text-field
|
||||
:model-value="alias"
|
||||
@update:model-value="updateAlias(index, $event)"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
class="flex-grow-1 mr-2"
|
||||
@update:model-value="updateAlias(index, $event)"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
variant="text"
|
||||
color="error"
|
||||
density="compact"
|
||||
@click="removeAlias(index)"
|
||||
/>
|
||||
<v-btn icon="mdi-delete" variant="text" color="error" density="compact" @click="removeAlias(index)" />
|
||||
</div>
|
||||
<v-btn
|
||||
prepend-icon="mdi-plus"
|
||||
@@ -114,7 +142,11 @@ watch(showAliasEditor, (open) => {
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="grey" variant="text" @click="emit('update:show', false)">
|
||||
<v-btn
|
||||
color="grey"
|
||||
variant="text"
|
||||
@click="emit('update:show', false)"
|
||||
>
|
||||
{{ tm('dialogs.rename.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
|
||||
@@ -40,39 +40,71 @@ const isInternal = (tool: ToolItem) => tool.source === 'internal';
|
||||
:loading="props.loading"
|
||||
>
|
||||
<template #item.name="{ item }">
|
||||
<div class="d-flex align-center py-2" :class="{ 'internal-tool-row': isInternal(item) }">
|
||||
<v-icon :color="isInternal(item) ? 'grey' : 'primary'" class="mr-2" size="18">
|
||||
<div
|
||||
class="d-flex align-center py-2"
|
||||
:class="{ 'internal-tool-row': isInternal(item) }"
|
||||
>
|
||||
<v-icon
|
||||
:color="isInternal(item) ? 'grey' : 'primary'"
|
||||
class="mr-2"
|
||||
size="18"
|
||||
>
|
||||
{{ isInternal(item) ? 'mdi-lock-outline' : (item.name.includes(':') ? 'mdi-server-network' : 'mdi-function-variant') }}
|
||||
</v-icon>
|
||||
<div>
|
||||
<div class="text-subtitle-1 font-weight-medium">{{ item.name }}</div>
|
||||
<div class="text-subtitle-1 font-weight-medium">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.description="{ item }">
|
||||
<div class="text-body-2 text-medium-emphasis" style="max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||||
<div
|
||||
class="text-body-2 text-medium-emphasis"
|
||||
style="max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
|
||||
>
|
||||
{{ item.description || '-' }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.origin="{ item }">
|
||||
<v-chip size="small" variant="tonal" color="info" class="text-caption font-weight-medium">
|
||||
<v-chip
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="info"
|
||||
class="text-caption font-weight-medium"
|
||||
>
|
||||
{{ item.origin || '-' }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template #item.origin_name="{ item }">
|
||||
<div class="text-body-2 text-medium-emphasis" style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||||
<div
|
||||
class="text-body-2 text-medium-emphasis"
|
||||
style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
|
||||
>
|
||||
{{ item.origin_name || '-' }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.active="{ item }">
|
||||
<v-chip v-if="isInternal(item)" color="grey" size="small" class="font-weight-medium" variant="tonal">
|
||||
<v-chip
|
||||
v-if="isInternal(item)"
|
||||
color="grey"
|
||||
size="small"
|
||||
class="font-weight-medium"
|
||||
variant="tonal"
|
||||
>
|
||||
内置
|
||||
</v-chip>
|
||||
<v-chip v-else :color="item.active ? 'success' : 'error'" size="small" class="font-weight-medium" :variant="item.active ? 'flat' : 'outlined'">
|
||||
<v-chip
|
||||
v-else
|
||||
:color="item.active ? 'success' : 'error'"
|
||||
size="small"
|
||||
class="font-weight-medium"
|
||||
:variant="item.active ? 'flat' : 'outlined'"
|
||||
>
|
||||
{{ item.active ? tmCommand('status.enabled') : tmCommand('status.disabled') }}
|
||||
</v-chip>
|
||||
</template>
|
||||
@@ -87,23 +119,47 @@ const isInternal = (tool: ToolItem) => tool.source === 'internal';
|
||||
inset
|
||||
@update:model-value="emit('toggle-tool', item)"
|
||||
/>
|
||||
<span v-else class="text-caption text-grey">—</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-caption text-grey"
|
||||
>—</span>
|
||||
</template>
|
||||
|
||||
<template #no-data>
|
||||
<div class="text-center pa-8">
|
||||
<v-icon size="64" color="info" class="mb-4">mdi-function-variant</v-icon>
|
||||
<div class="text-h5 mb-2">{{ tmTool('functionTools.empty') }}</div>
|
||||
<v-icon
|
||||
size="64"
|
||||
color="info"
|
||||
class="mb-4"
|
||||
>
|
||||
mdi-function-variant
|
||||
</v-icon>
|
||||
<div class="text-h5 mb-2">
|
||||
{{ tmTool('functionTools.empty') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #expanded-row="{ item }">
|
||||
<td :colspan="toolHeaders.length + 1" class="pa-4">
|
||||
<td
|
||||
:colspan="toolHeaders.length + 1"
|
||||
class="pa-4"
|
||||
>
|
||||
<div class="d-flex align-start ga-4">
|
||||
<v-icon size="20" color="primary">mdi-code-json</v-icon>
|
||||
<v-icon
|
||||
size="20"
|
||||
color="primary"
|
||||
>
|
||||
mdi-code-json
|
||||
</v-icon>
|
||||
<div class="flex-1">
|
||||
<div class="text-subtitle-2 font-weight-medium mb-2">{{ tmTool('functionTools.parameters') }}</div>
|
||||
<div v-if="parameterEntries(item).length === 0" class="text-caption text-medium-emphasis">
|
||||
<div class="text-subtitle-2 font-weight-medium mb-2">
|
||||
{{ tmTool('functionTools.parameters') }}
|
||||
</div>
|
||||
<div
|
||||
v-if="parameterEntries(item).length === 0"
|
||||
class="text-caption text-medium-emphasis"
|
||||
>
|
||||
{{ tmTool('functionTools.noParameters') }}
|
||||
</div>
|
||||
<v-table
|
||||
@@ -113,20 +169,40 @@ const isInternal = (tool: ToolItem) => tool.source === 'internal';
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left text-caption text-medium-emphasis">{{ tmTool('functionTools.table.paramName') }}</th>
|
||||
<th class="text-left text-caption text-medium-emphasis" style="width: 140px;">{{ tmTool('functionTools.table.type') }}</th>
|
||||
<th class="text-left text-caption text-medium-emphasis">{{ tmTool('functionTools.table.description') }}</th>
|
||||
<th class="text-left text-caption text-medium-emphasis">
|
||||
{{ tmTool('functionTools.table.paramName') }}
|
||||
</th>
|
||||
<th
|
||||
class="text-left text-caption text-medium-emphasis"
|
||||
style="width: 140px;"
|
||||
>
|
||||
{{ tmTool('functionTools.table.type') }}
|
||||
</th>
|
||||
<th class="text-left text-caption text-medium-emphasis">
|
||||
{{ tmTool('functionTools.table.description') }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="([paramName, param]) in parameterEntries(item)" :key="paramName">
|
||||
<td class="font-weight-medium text-body-2">{{ paramName }}</td>
|
||||
<tr
|
||||
v-for="([paramName, param]) in parameterEntries(item)"
|
||||
:key="paramName"
|
||||
>
|
||||
<td class="font-weight-medium text-body-2">
|
||||
{{ paramName }}
|
||||
</td>
|
||||
<td class="text-body-2">
|
||||
<v-chip size="x-small" color="primary" class="text-caption">
|
||||
<v-chip
|
||||
size="x-small"
|
||||
color="primary"
|
||||
class="text-caption"
|
||||
>
|
||||
{{ param?.type || '-' }}
|
||||
</v-chip>
|
||||
</td>
|
||||
<td class="text-body-2 text-medium-emphasis">{{ param?.description || '-' }}</td>
|
||||
<td class="text-body-2 text-medium-emphasis">
|
||||
{{ param?.description || '-' }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
|
||||
@@ -24,7 +24,7 @@ export function useComponentData() {
|
||||
/**
|
||||
* 显示 Toast 消息
|
||||
*/
|
||||
const toast = (message: string, color: string = 'success') => {
|
||||
const toast = (message: string, color = 'success') => {
|
||||
snackbar.message = message;
|
||||
snackbar.color = color;
|
||||
snackbar.show = true;
|
||||
|
||||
@@ -155,16 +155,35 @@ watch(viewMode, async (mode) => {
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card variant="flat" style="background-color: transparent">
|
||||
<v-card
|
||||
variant="flat"
|
||||
style="background-color: transparent"
|
||||
>
|
||||
<v-card-text style="padding: 20px 12px; padding-top: 0px;">
|
||||
<div class="d-flex justify-space-between align-center mb-6 flex-wrap ga-3">
|
||||
<v-btn-toggle v-model="viewMode" color="primary" variant="outlined" density="comfortable" mandatory>
|
||||
<v-btn-toggle
|
||||
v-model="viewMode"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
mandatory
|
||||
>
|
||||
<v-btn value="commands">
|
||||
<v-icon size="18" class="mr-1">mdi-console-line</v-icon>
|
||||
<v-icon
|
||||
size="18"
|
||||
class="mr-1"
|
||||
>
|
||||
mdi-console-line
|
||||
</v-icon>
|
||||
{{ tm('type.command') }}
|
||||
</v-btn>
|
||||
<v-btn value="tools">
|
||||
<v-icon size="18" class="mr-1">mdi-function-variant</v-icon>
|
||||
<v-icon
|
||||
size="18"
|
||||
class="mr-1"
|
||||
>
|
||||
mdi-function-variant
|
||||
</v-icon>
|
||||
{{ tmTool('functionTools.title') }}
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
@@ -185,30 +204,46 @@ watch(viewMode, async (mode) => {
|
||||
<div v-if="viewMode === 'commands'">
|
||||
<CommandFilters
|
||||
:plugin-filter="pluginFilter"
|
||||
@update:plugin-filter="pluginFilter = $event"
|
||||
:type-filter="typeFilter"
|
||||
@update:type-filter="typeFilter = $event"
|
||||
:permission-filter="permissionFilter"
|
||||
@update:permission-filter="permissionFilter = $event"
|
||||
:status-filter="statusFilter"
|
||||
@update:status-filter="statusFilter = $event"
|
||||
:show-system-plugins="showSystemPlugins"
|
||||
@update:show-system-plugins="showSystemPlugins = $event"
|
||||
:search-query="searchQuery"
|
||||
@update:search-query="searchQuery = $event"
|
||||
:available-plugins="availablePlugins"
|
||||
:has-system-plugin-conflict="hasSystemPluginConflict"
|
||||
:effective-show-system-plugins="effectiveShowSystemPlugins"
|
||||
@update:plugin-filter="pluginFilter = $event"
|
||||
@update:type-filter="typeFilter = $event"
|
||||
@update:permission-filter="permissionFilter = $event"
|
||||
@update:status-filter="statusFilter = $event"
|
||||
@update:show-system-plugins="showSystemPlugins = $event"
|
||||
@update:search-query="searchQuery = $event"
|
||||
>
|
||||
<template #stats>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="18" color="primary" class="mr-1">mdi-console-line</v-icon>
|
||||
<v-icon
|
||||
size="18"
|
||||
color="primary"
|
||||
class="mr-1"
|
||||
>
|
||||
mdi-console-line
|
||||
</v-icon>
|
||||
<span class="text-body-2 text-medium-emphasis mr-1">{{ tm('summary.total') }}:</span>
|
||||
<span class="text-body-1 font-weight-bold text-primary">{{ filteredCommands.length }}</span>
|
||||
</div>
|
||||
<v-divider vertical class="mx-1" style="height: 20px;" />
|
||||
<v-divider
|
||||
vertical
|
||||
class="mx-1"
|
||||
style="height: 20px;"
|
||||
/>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="18" color="error" class="mr-1">mdi-close-circle-outline</v-icon>
|
||||
<v-icon
|
||||
size="18"
|
||||
color="error"
|
||||
class="mr-1"
|
||||
>
|
||||
mdi-close-circle-outline
|
||||
</v-icon>
|
||||
<span class="text-body-2 text-medium-emphasis mr-1">{{ tm('summary.disabled') }}:</span>
|
||||
<span class="text-body-1 font-weight-bold text-error">{{ summary.disabled }}</span>
|
||||
</div>
|
||||
@@ -223,8 +258,10 @@ watch(viewMode, async (mode) => {
|
||||
prominent
|
||||
border="start"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="28">mdi-alert-circle</v-icon>
|
||||
<template #prepend>
|
||||
<v-icon size="28">
|
||||
mdi-alert-circle
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-alert-title class="text-subtitle-1 font-weight-bold">
|
||||
{{ tm('conflictAlert.title') }}
|
||||
@@ -233,7 +270,12 @@ watch(viewMode, async (mode) => {
|
||||
{{ tm('conflictAlert.description', { count: summary.conflicts }) }}
|
||||
</div>
|
||||
<div class="text-body-2 mt-2">
|
||||
<v-icon size="16" class="mr-1">mdi-lightbulb-outline</v-icon>
|
||||
<v-icon
|
||||
size="16"
|
||||
class="mr-1"
|
||||
>
|
||||
mdi-lightbulb-outline
|
||||
</v-icon>
|
||||
{{ tm('conflictAlert.hint') }}
|
||||
</div>
|
||||
</v-alert>
|
||||
@@ -255,24 +297,40 @@ watch(viewMode, async (mode) => {
|
||||
<div style="min-width: 240px; max-width: 380px; flex: 1;">
|
||||
<v-text-field
|
||||
:model-value="toolSearch"
|
||||
@update:model-value="toolSearch = normalizeTextInput($event)"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
:label="tmTool('functionTools.search')"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
clearable
|
||||
@update:model-value="toolSearch = normalizeTextInput($event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex align-center ga-2">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="18" color="primary" class="mr-1">mdi-function-variant</v-icon>
|
||||
<v-icon
|
||||
size="18"
|
||||
color="primary"
|
||||
class="mr-1"
|
||||
>
|
||||
mdi-function-variant
|
||||
</v-icon>
|
||||
<span class="text-body-2 text-medium-emphasis mr-1">{{ tm('summary.total') }}:</span>
|
||||
<span class="text-body-1 font-weight-bold text-primary">{{ filteredTools.length }}</span>
|
||||
</div>
|
||||
<v-divider vertical class="mx-1" style="height: 20px;" />
|
||||
<v-divider
|
||||
vertical
|
||||
class="mx-1"
|
||||
style="height: 20px;"
|
||||
/>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="18" color="success" class="mr-1">mdi-check-circle-outline</v-icon>
|
||||
<v-icon
|
||||
size="18"
|
||||
color="success"
|
||||
class="mr-1"
|
||||
>
|
||||
mdi-check-circle-outline
|
||||
</v-icon>
|
||||
<span class="text-body-2 text-medium-emphasis mr-1">{{ tm('status.enabled') }}:</span>
|
||||
<span class="text-body-1 font-weight-bold text-success">{{ filteredTools.filter(t => t.active).length }}</span>
|
||||
</div>
|
||||
@@ -293,25 +351,30 @@ watch(viewMode, async (mode) => {
|
||||
<!-- 重命名对话框 -->
|
||||
<RenameDialog
|
||||
:show="renameDialog.show"
|
||||
@update:show="renameDialog.show = $event"
|
||||
:new-name="renameDialog.newName"
|
||||
@update:new-name="renameDialog.newName = $event"
|
||||
:aliases="renameDialog.aliases"
|
||||
@update:aliases="renameDialog.aliases = $event"
|
||||
:command="renameDialog.command"
|
||||
:loading="renameDialog.loading"
|
||||
@update:show="renameDialog.show = $event"
|
||||
@update:new-name="renameDialog.newName = $event"
|
||||
@update:aliases="renameDialog.aliases = $event"
|
||||
@confirm="handleConfirmRename"
|
||||
/>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<DetailsDialog
|
||||
:show="detailsDialog.show"
|
||||
@update:show="detailsDialog.show = $event"
|
||||
:command="detailsDialog.command"
|
||||
@update:show="detailsDialog.show = $event"
|
||||
/>
|
||||
|
||||
<!-- Snackbar -->
|
||||
<v-snackbar :timeout="2000" elevation="24" :color="snackbar.color" v-model="snackbar.show">
|
||||
<v-snackbar
|
||||
v-model="snackbar.show"
|
||||
:timeout="2000"
|
||||
elevation="24"
|
||||
:color="snackbar.color"
|
||||
>
|
||||
{{ snackbar.message }}
|
||||
</v-snackbar>
|
||||
</template>
|
||||
|
||||
@@ -1,31 +1,60 @@
|
||||
<template>
|
||||
<v-dialog v-model="showDialog" max-width="450px">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<v-icon class="mr-2">mdi-folder-plus</v-icon>
|
||||
{{ labels.title }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-form ref="form" v-model="formValid">
|
||||
<v-text-field v-model="formData.name" :label="mergedLabels.nameLabel"
|
||||
:rules="[(v: any) => !!v || mergedLabels.nameRequired]" variant="outlined"
|
||||
density="comfortable" autofocus class="mb-3" />
|
||||
<v-dialog
|
||||
v-model="showDialog"
|
||||
max-width="450px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<v-icon class="mr-2">
|
||||
mdi-folder-plus
|
||||
</v-icon>
|
||||
{{ labels.title }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-form
|
||||
ref="form"
|
||||
v-model="formValid"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="formData.name"
|
||||
:label="mergedLabels.nameLabel"
|
||||
:rules="[(v: any) => !!v || mergedLabels.nameRequired]"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
autofocus
|
||||
class="mb-3"
|
||||
/>
|
||||
|
||||
<v-textarea v-model="formData.description" :label="labels.descriptionLabel" variant="outlined"
|
||||
rows="3" density="comfortable" hide-details />
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="closeDialog">
|
||||
{{ labels.cancelButton }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" variant="flat" @click="submitForm" :loading="loading" :disabled="!formValid">
|
||||
{{ labels.createButton }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-textarea
|
||||
v-model="formData.description"
|
||||
:label="labels.descriptionLabel"
|
||||
variant="outlined"
|
||||
rows="3"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="closeDialog"
|
||||
>
|
||||
{{ labels.cancelButton }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="flat"
|
||||
:loading="loading"
|
||||
:disabled="!formValid"
|
||||
@click="submitForm"
|
||||
>
|
||||
{{ labels.createButton }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -1,19 +1,38 @@
|
||||
<template>
|
||||
<v-breadcrumbs :items="computedItems" class="base-folder-breadcrumb pa-0">
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="small" class="mr-1">mdi-folder-outline</v-icon>
|
||||
</template>
|
||||
<template v-slot:item="{ item }">
|
||||
<v-breadcrumbs-item :disabled="(item as any).disabled" @click="!(item as any).disabled && handleClick((item as any).folderId)"
|
||||
:class="{ 'breadcrumb-link': !(item as any).disabled }">
|
||||
<v-icon v-if="(item as any).isRoot" size="small" class="mr-1">mdi-home</v-icon>
|
||||
{{ (item as any).title }}
|
||||
</v-breadcrumbs-item>
|
||||
</template>
|
||||
<template v-slot:divider>
|
||||
<v-icon size="small">mdi-chevron-right</v-icon>
|
||||
</template>
|
||||
</v-breadcrumbs>
|
||||
<v-breadcrumbs
|
||||
:items="computedItems"
|
||||
class="base-folder-breadcrumb pa-0"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
size="small"
|
||||
class="mr-1"
|
||||
>
|
||||
mdi-folder-outline
|
||||
</v-icon>
|
||||
</template>
|
||||
<template #item="{ item }">
|
||||
<v-breadcrumbs-item
|
||||
:disabled="(item as any).disabled"
|
||||
:class="{ 'breadcrumb-link': !(item as any).disabled }"
|
||||
@click="!(item as any).disabled && handleClick((item as any).folderId)"
|
||||
>
|
||||
<v-icon
|
||||
v-if="(item as any).isRoot"
|
||||
size="small"
|
||||
class="mr-1"
|
||||
>
|
||||
mdi-home
|
||||
</v-icon>
|
||||
{{ (item as any).title }}
|
||||
</v-breadcrumbs-item>
|
||||
</template>
|
||||
<template #divider>
|
||||
<v-icon size="small">
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-breadcrumbs>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -1,48 +1,89 @@
|
||||
<template>
|
||||
<v-card class="base-folder-card" :class="{ 'drag-over': isDragOver }" rounded="lg" @click="$emit('click')" @contextmenu.prevent="$emit('contextmenu', $event)"
|
||||
elevation="1" hover @dragover.prevent="handleDragOver" @dragleave="handleDragLeave" @drop.prevent="handleDrop">
|
||||
<v-card-text class="d-flex align-center pa-3">
|
||||
<v-icon size="40" color="amber-darken-2" class="mr-3">mdi-folder</v-icon>
|
||||
<div class="folder-info flex-grow-1 overflow-hidden">
|
||||
<div class="text-subtitle-1 font-weight-medium text-truncate">{{ folder.name }}</div>
|
||||
<div v-if="folder.description" class="text-body-2 text-medium-emphasis text-truncate">
|
||||
{{ folder.description }}
|
||||
</div>
|
||||
</div>
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn icon="mdi-dots-vertical" variant="text" size="small" v-bind="props" @click.stop />
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item @click.stop="$emit('open')">
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="small">mdi-folder-open</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ labels.open }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click.stop="$emit('rename')">
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="small">mdi-pencil</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ labels.rename }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click.stop="$emit('move')">
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="small">mdi-folder-move</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ labels.moveTo }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider class="my-1" />
|
||||
<v-list-item @click.stop="$emit('delete')" class="text-error">
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="small" color="error">mdi-delete</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ labels.delete }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-card
|
||||
class="base-folder-card"
|
||||
:class="{ 'drag-over': isDragOver }"
|
||||
rounded="lg"
|
||||
elevation="1"
|
||||
hover
|
||||
@click="$emit('click')"
|
||||
@contextmenu.prevent="$emit('contextmenu', $event)"
|
||||
@dragover.prevent="handleDragOver"
|
||||
@dragleave="handleDragLeave"
|
||||
@drop.prevent="handleDrop"
|
||||
>
|
||||
<v-card-text class="d-flex align-center pa-3">
|
||||
<v-icon
|
||||
size="40"
|
||||
color="amber-darken-2"
|
||||
class="mr-3"
|
||||
>
|
||||
mdi-folder
|
||||
</v-icon>
|
||||
<div class="folder-info flex-grow-1 overflow-hidden">
|
||||
<div class="text-subtitle-1 font-weight-medium text-truncate">
|
||||
{{ folder.name }}
|
||||
</div>
|
||||
<div
|
||||
v-if="folder.description"
|
||||
class="text-body-2 text-medium-emphasis text-truncate"
|
||||
>
|
||||
{{ folder.description }}
|
||||
</div>
|
||||
</div>
|
||||
<v-menu offset-y>
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
icon="mdi-dots-vertical"
|
||||
variant="text"
|
||||
size="small"
|
||||
v-bind="props"
|
||||
@click.stop
|
||||
/>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item @click.stop="$emit('open')">
|
||||
<template #prepend>
|
||||
<v-icon size="small">
|
||||
mdi-folder-open
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ labels.open }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click.stop="$emit('rename')">
|
||||
<template #prepend>
|
||||
<v-icon size="small">
|
||||
mdi-pencil
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ labels.rename }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click.stop="$emit('move')">
|
||||
<template #prepend>
|
||||
<v-icon size="small">
|
||||
mdi-folder-move
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ labels.moveTo }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider class="my-1" />
|
||||
<v-list-item
|
||||
class="text-error"
|
||||
@click.stop="$emit('delete')"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
size="small"
|
||||
color="error"
|
||||
>
|
||||
mdi-delete
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ labels.delete }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -1,165 +1,318 @@
|
||||
<template>
|
||||
<div class="folder-item-selector">
|
||||
<!-- 触发按钮区域 -->
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<span v-if="!modelValue" style="color: rgb(var(--v-theme-primaryText));">
|
||||
{{ labels.notSelected || '未选择' }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ displayValue }}
|
||||
</span>
|
||||
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
|
||||
{{ labels.buttonText || '选择...' }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- 选择对话框 -->
|
||||
<v-dialog v-model="dialog" max-width="1000px" min-width="800px">
|
||||
<v-card class="selector-dialog-card">
|
||||
<v-card-title class="dialog-title d-flex align-center py-4 px-5">
|
||||
<v-icon class="mr-3" color="primary">mdi-account-circle</v-icon>
|
||||
<span>{{ labels.dialogTitle || '选择项目' }}</span>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-text class="pa-0" style="height: 600px; max-height: 80vh; overflow: hidden;">
|
||||
<div class="selector-layout">
|
||||
<!-- 左侧文件夹树 -->
|
||||
<div class="folder-sidebar">
|
||||
<div class="sidebar-header pa-3 pb-2">
|
||||
<span class="text-caption text-medium-emphasis font-weight-medium">
|
||||
<v-icon size="small" class="mr-1">mdi-folder-multiple</v-icon>
|
||||
文件夹
|
||||
</span>
|
||||
</div>
|
||||
<v-list density="compact" nav class="tree-list pa-2" bg-color="transparent">
|
||||
<!-- 根目录 -->
|
||||
<v-list-item :active="currentFolderId === null" @click="navigateToFolder(null)"
|
||||
rounded="lg" class="mb-1 root-item">
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="20" :color="currentFolderId === null ? 'primary' : ''">mdi-home</v-icon>
|
||||
</template>
|
||||
<v-list-item-title class="text-body-2">{{ labels.rootFolder || '根目录' }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<!-- 文件夹树 -->
|
||||
<template v-if="!treeLoading">
|
||||
<BaseMoveTargetNode v-for="folder in folderTree" :key="folder.folder_id"
|
||||
:folder="folder" :depth="0" :selected-folder-id="currentFolderId"
|
||||
:disabled-folder-ids="[]" @select="navigateToFolder" />
|
||||
</template>
|
||||
|
||||
<div v-if="treeLoading" class="text-center pa-4">
|
||||
<v-progress-circular indeterminate size="20" color="primary" />
|
||||
</div>
|
||||
</v-list>
|
||||
</div>
|
||||
|
||||
<!-- 右侧项目列表 -->
|
||||
<div class="items-panel">
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="breadcrumb-bar px-4 py-3">
|
||||
<v-breadcrumbs :items="breadcrumbItems" density="compact" class="pa-0">
|
||||
<template v-slot:item="{ item }">
|
||||
<v-breadcrumbs-item :disabled="(item as any).disabled"
|
||||
@click="!(item as any).disabled && navigateToFolder((item as any).folderId)"
|
||||
:class="{ 'breadcrumb-link': !(item as any).disabled }">
|
||||
<v-icon v-if="(item as any).isRoot" size="small"
|
||||
class="mr-1">mdi-home</v-icon>
|
||||
{{ item.title }}
|
||||
</v-breadcrumbs-item>
|
||||
</template>
|
||||
<template v-slot:divider>
|
||||
<v-icon size="small" color="grey">mdi-chevron-right</v-icon>
|
||||
</template>
|
||||
</v-breadcrumbs>
|
||||
</div>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<!-- 项目列表 -->
|
||||
<div class="items-list">
|
||||
<v-progress-linear v-if="itemsLoading" indeterminate
|
||||
color="primary" height="2"></v-progress-linear>
|
||||
|
||||
<!-- 子文件夹 -->
|
||||
<v-list v-if="!itemsLoading" lines="two" class="pa-3 items-content">
|
||||
<template v-if="currentSubFolders.length > 0">
|
||||
<div class="section-label text-caption text-medium-emphasis mb-2 px-2">子文件夹</div>
|
||||
<v-list-item v-for="folder in currentSubFolders" :key="'folder-' + folder.folder_id"
|
||||
@click="navigateToFolder(folder.folder_id)" rounded="lg" class="mb-1 folder-item">
|
||||
<template v-slot:prepend>
|
||||
<v-avatar size="36" color="amber-lighten-4" class="mr-3">
|
||||
<v-icon color="amber-darken-2" size="20">mdi-folder</v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<v-list-item-title class="font-weight-medium">{{ folder.name }}</v-list-item-title>
|
||||
<template v-slot:append>
|
||||
<v-icon size="20" color="grey">mdi-chevron-right</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<!-- 项目列表 -->
|
||||
<template v-if="currentItems.length > 0">
|
||||
<div class="section-label text-caption text-medium-emphasis mb-2 px-2" :class="{ 'mt-4': currentSubFolders.length > 0 }">可选项目</div>
|
||||
<v-list-item v-for="item in currentItems" :key="'item-' + getItemId(item)"
|
||||
:value="getItemId(item)" @click="selectItem(item)"
|
||||
:active="selectedItemId === getItemId(item)" rounded="lg" class="mb-1 persona-item"
|
||||
:class="{ 'selected-item': selectedItemId === getItemId(item) }">
|
||||
<template v-slot:prepend>
|
||||
<v-avatar size="36" :color="selectedItemId === getItemId(item) ? 'primary-lighten-4' : 'grey-lighten-3'" class="mr-3">
|
||||
<v-icon :color="selectedItemId === getItemId(item) ? 'primary' : 'grey-darken-1'" size="20">mdi-account</v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<v-list-item-title class="font-weight-medium">{{ getItemName(item) }}</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="getItemDescription(item)" class="text-truncate">
|
||||
{{ truncateText(getItemDescription(item), 80) }}
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<template v-slot:append>
|
||||
<div class="d-flex align-center ga-1">
|
||||
<v-btn v-if="showEditButton && !isDefaultItem(item)"
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click.stop="handleEditItem(item)"
|
||||
:title="labels.editButton || 'Edit'"
|
||||
/>
|
||||
<v-icon v-if="selectedItemId === getItemId(item)"
|
||||
color="primary" size="22">mdi-check-circle</v-icon>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="currentSubFolders.length === 0 && currentItems.length === 0"
|
||||
class="empty-state text-center py-12">
|
||||
<v-icon size="64" color="grey-lighten-2">mdi-folder-open-outline</v-icon>
|
||||
<p class="text-grey mt-4 text-body-2">{{ labels.emptyFolder || labels.noItems || '此文件夹为空' }}</p>
|
||||
</div>
|
||||
</v-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-btn v-if="showCreateButton" variant="text" color="primary" prepend-icon="mdi-plus"
|
||||
@click="$emit('create')">
|
||||
{{ labels.createButton || '新建' }}
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="cancelSelection">{{ labels.cancelButton || '取消' }}</v-btn>
|
||||
<v-btn color="primary" @click="confirmSelection" :disabled="!selectedItemId">
|
||||
{{ labels.confirmButton || '确认' }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<div class="folder-item-selector">
|
||||
<!-- 触发按钮区域 -->
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<span
|
||||
v-if="!modelValue"
|
||||
style="color: rgb(var(--v-theme-primaryText));"
|
||||
>
|
||||
{{ labels.notSelected || '未选择' }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ displayValue }}
|
||||
</span>
|
||||
<v-btn
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
@click="openDialog"
|
||||
>
|
||||
{{ labels.buttonText || '选择...' }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- 选择对话框 -->
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
max-width="1000px"
|
||||
min-width="800px"
|
||||
>
|
||||
<v-card class="selector-dialog-card">
|
||||
<v-card-title class="dialog-title d-flex align-center py-4 px-5">
|
||||
<v-icon
|
||||
class="mr-3"
|
||||
color="primary"
|
||||
>
|
||||
mdi-account-circle
|
||||
</v-icon>
|
||||
<span>{{ labels.dialogTitle || '选择项目' }}</span>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-text
|
||||
class="pa-0"
|
||||
style="height: 600px; max-height: 80vh; overflow: hidden;"
|
||||
>
|
||||
<div class="selector-layout">
|
||||
<!-- 左侧文件夹树 -->
|
||||
<div class="folder-sidebar">
|
||||
<div class="sidebar-header pa-3 pb-2">
|
||||
<span class="text-caption text-medium-emphasis font-weight-medium">
|
||||
<v-icon
|
||||
size="small"
|
||||
class="mr-1"
|
||||
>mdi-folder-multiple</v-icon>
|
||||
文件夹
|
||||
</span>
|
||||
</div>
|
||||
<v-list
|
||||
density="compact"
|
||||
nav
|
||||
class="tree-list pa-2"
|
||||
bg-color="transparent"
|
||||
>
|
||||
<!-- 根目录 -->
|
||||
<v-list-item
|
||||
:active="currentFolderId === null"
|
||||
rounded="lg"
|
||||
class="mb-1 root-item"
|
||||
@click="navigateToFolder(null)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
size="20"
|
||||
:color="currentFolderId === null ? 'primary' : ''"
|
||||
>
|
||||
mdi-home
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title class="text-body-2">
|
||||
{{ labels.rootFolder || '根目录' }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<!-- 文件夹树 -->
|
||||
<template v-if="!treeLoading">
|
||||
<BaseMoveTargetNode
|
||||
v-for="folder in folderTree"
|
||||
:key="folder.folder_id"
|
||||
:folder="folder"
|
||||
:depth="0"
|
||||
:selected-folder-id="currentFolderId"
|
||||
:disabled-folder-ids="[]"
|
||||
@select="navigateToFolder"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-if="treeLoading"
|
||||
class="text-center pa-4"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
size="20"
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
</v-list>
|
||||
</div>
|
||||
|
||||
<!-- 右侧项目列表 -->
|
||||
<div class="items-panel">
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="breadcrumb-bar px-4 py-3">
|
||||
<v-breadcrumbs
|
||||
:items="breadcrumbItems"
|
||||
density="compact"
|
||||
class="pa-0"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<v-breadcrumbs-item
|
||||
:disabled="(item as any).disabled"
|
||||
:class="{ 'breadcrumb-link': !(item as any).disabled }"
|
||||
@click="!(item as any).disabled && navigateToFolder((item as any).folderId)"
|
||||
>
|
||||
<v-icon
|
||||
v-if="(item as any).isRoot"
|
||||
size="small"
|
||||
class="mr-1"
|
||||
>
|
||||
mdi-home
|
||||
</v-icon>
|
||||
{{ item.title }}
|
||||
</v-breadcrumbs-item>
|
||||
</template>
|
||||
<template #divider>
|
||||
<v-icon
|
||||
size="small"
|
||||
color="grey"
|
||||
>
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-breadcrumbs>
|
||||
</div>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<!-- 项目列表 -->
|
||||
<div class="items-list">
|
||||
<v-progress-linear
|
||||
v-if="itemsLoading"
|
||||
indeterminate
|
||||
color="primary"
|
||||
height="2"
|
||||
/>
|
||||
|
||||
<!-- 子文件夹 -->
|
||||
<v-list
|
||||
v-if="!itemsLoading"
|
||||
lines="two"
|
||||
class="pa-3 items-content"
|
||||
>
|
||||
<template v-if="currentSubFolders.length > 0">
|
||||
<div class="section-label text-caption text-medium-emphasis mb-2 px-2">
|
||||
子文件夹
|
||||
</div>
|
||||
<v-list-item
|
||||
v-for="folder in currentSubFolders"
|
||||
:key="'folder-' + folder.folder_id"
|
||||
rounded="lg"
|
||||
class="mb-1 folder-item"
|
||||
@click="navigateToFolder(folder.folder_id)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-avatar
|
||||
size="36"
|
||||
color="amber-lighten-4"
|
||||
class="mr-3"
|
||||
>
|
||||
<v-icon
|
||||
color="amber-darken-2"
|
||||
size="20"
|
||||
>
|
||||
mdi-folder
|
||||
</v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<v-list-item-title class="font-weight-medium">
|
||||
{{ folder.name }}
|
||||
</v-list-item-title>
|
||||
<template #append>
|
||||
<v-icon
|
||||
size="20"
|
||||
color="grey"
|
||||
>
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<!-- 项目列表 -->
|
||||
<template v-if="currentItems.length > 0">
|
||||
<div
|
||||
class="section-label text-caption text-medium-emphasis mb-2 px-2"
|
||||
:class="{ 'mt-4': currentSubFolders.length > 0 }"
|
||||
>
|
||||
可选项目
|
||||
</div>
|
||||
<v-list-item
|
||||
v-for="item in currentItems"
|
||||
:key="'item-' + getItemId(item)"
|
||||
:value="getItemId(item)"
|
||||
:active="selectedItemId === getItemId(item)"
|
||||
rounded="lg"
|
||||
class="mb-1 persona-item"
|
||||
:class="{ 'selected-item': selectedItemId === getItemId(item) }"
|
||||
@click="selectItem(item)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-avatar
|
||||
size="36"
|
||||
:color="selectedItemId === getItemId(item) ? 'primary-lighten-4' : 'grey-lighten-3'"
|
||||
class="mr-3"
|
||||
>
|
||||
<v-icon
|
||||
:color="selectedItemId === getItemId(item) ? 'primary' : 'grey-darken-1'"
|
||||
size="20"
|
||||
>
|
||||
mdi-account
|
||||
</v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<v-list-item-title class="font-weight-medium">
|
||||
{{ getItemName(item) }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle
|
||||
v-if="getItemDescription(item)"
|
||||
class="text-truncate"
|
||||
>
|
||||
{{ truncateText(getItemDescription(item), 80) }}
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<template #append>
|
||||
<div class="d-flex align-center ga-1">
|
||||
<v-btn
|
||||
v-if="showEditButton && !isDefaultItem(item)"
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
variant="text"
|
||||
:title="labels.editButton || 'Edit'"
|
||||
@click.stop="handleEditItem(item)"
|
||||
/>
|
||||
<v-icon
|
||||
v-if="selectedItemId === getItemId(item)"
|
||||
color="primary"
|
||||
size="22"
|
||||
>
|
||||
mdi-check-circle
|
||||
</v-icon>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div
|
||||
v-if="currentSubFolders.length === 0 && currentItems.length === 0"
|
||||
class="empty-state text-center py-12"
|
||||
>
|
||||
<v-icon
|
||||
size="64"
|
||||
color="grey-lighten-2"
|
||||
>
|
||||
mdi-folder-open-outline
|
||||
</v-icon>
|
||||
<p class="text-grey mt-4 text-body-2">
|
||||
{{ labels.emptyFolder || labels.noItems || '此文件夹为空' }}
|
||||
</p>
|
||||
</div>
|
||||
</v-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-btn
|
||||
v-if="showCreateButton"
|
||||
variant="text"
|
||||
color="primary"
|
||||
prepend-icon="mdi-plus"
|
||||
@click="$emit('create')"
|
||||
>
|
||||
{{ labels.createButton || '新建' }}
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="cancelSelection"
|
||||
>
|
||||
{{ labels.cancelButton || '取消' }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
:disabled="!selectedItemId"
|
||||
@click="confirmSelection"
|
||||
>
|
||||
{{ labels.confirmButton || '确认' }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -1,74 +1,136 @@
|
||||
<template>
|
||||
<div class="base-folder-tree">
|
||||
<!-- 搜索框 -->
|
||||
<v-text-field v-model="searchQuery" :placeholder="labels.searchPlaceholder" prepend-inner-icon="mdi-magnify"
|
||||
variant="outlined" density="compact" hide-details clearable class="mb-3" />
|
||||
<div class="base-folder-tree">
|
||||
<!-- 搜索框 -->
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
:placeholder="labels.searchPlaceholder"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
clearable
|
||||
class="mb-3"
|
||||
/>
|
||||
|
||||
<!-- 根目录节点 -->
|
||||
<v-list density="compact" nav class="tree-list" bg-color="transparent">
|
||||
<v-list-item :active="currentFolderId === null" @click="handleFolderClick(null)" rounded="lg"
|
||||
:class="['root-item', { 'drag-over': isRootDragOver }]"
|
||||
@dragover.prevent="handleRootDragOver" @dragleave="handleRootDragLeave" @drop.prevent="handleRootDrop">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-home</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ labels.rootFolder }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<!-- 根目录节点 -->
|
||||
<v-list
|
||||
density="compact"
|
||||
nav
|
||||
class="tree-list"
|
||||
bg-color="transparent"
|
||||
>
|
||||
<v-list-item
|
||||
:active="currentFolderId === null"
|
||||
rounded="lg"
|
||||
:class="['root-item', { 'drag-over': isRootDragOver }]"
|
||||
@click="handleFolderClick(null)"
|
||||
@dragover.prevent="handleRootDragOver"
|
||||
@dragleave="handleRootDragLeave"
|
||||
@drop.prevent="handleRootDrop"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-home</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ labels.rootFolder }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<!-- 文件夹树 -->
|
||||
<template v-if="!treeLoading">
|
||||
<BaseFolderTreeNode v-for="folder in filteredFolderTree" :key="folder.folder_id" :folder="folder"
|
||||
:depth="0" :current-folder-id="currentFolderId" :search-query="searchQuery"
|
||||
:expanded-folder-ids="expandedFolderIds" :accept-drop-types="acceptDropTypes"
|
||||
@folder-click="handleFolderClick" @folder-context-menu="handleContextMenu"
|
||||
@item-dropped="$emit('item-dropped', $event)"
|
||||
@toggle-expansion="$emit('toggle-expansion', $event)"
|
||||
@set-expansion="$emit('set-expansion', $event)" />
|
||||
</template>
|
||||
<!-- 文件夹树 -->
|
||||
<template v-if="!treeLoading">
|
||||
<BaseFolderTreeNode
|
||||
v-for="folder in filteredFolderTree"
|
||||
:key="folder.folder_id"
|
||||
:folder="folder"
|
||||
:depth="0"
|
||||
:current-folder-id="currentFolderId"
|
||||
:search-query="searchQuery"
|
||||
:expanded-folder-ids="expandedFolderIds"
|
||||
:accept-drop-types="acceptDropTypes"
|
||||
@folder-click="handleFolderClick"
|
||||
@folder-context-menu="handleContextMenu"
|
||||
@item-dropped="$emit('item-dropped', $event)"
|
||||
@toggle-expansion="$emit('toggle-expansion', $event)"
|
||||
@set-expansion="$emit('set-expansion', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="treeLoading" class="text-center pa-4">
|
||||
<v-progress-circular indeterminate size="24" />
|
||||
</div>
|
||||
<!-- 加载状态 -->
|
||||
<div
|
||||
v-if="treeLoading"
|
||||
class="text-center pa-4"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
size="24"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="!treeLoading && folderTree.length === 0" class="text-center pa-4 text-medium-emphasis">
|
||||
<v-icon size="32" class="mb-2">mdi-folder-outline</v-icon>
|
||||
<div class="text-body-2">{{ labels.noFolders }}</div>
|
||||
</div>
|
||||
</v-list>
|
||||
<!-- 空状态 -->
|
||||
<div
|
||||
v-if="!treeLoading && folderTree.length === 0"
|
||||
class="text-center pa-4 text-medium-emphasis"
|
||||
>
|
||||
<v-icon
|
||||
size="32"
|
||||
class="mb-2"
|
||||
>
|
||||
mdi-folder-outline
|
||||
</v-icon>
|
||||
<div class="text-body-2">
|
||||
{{ labels.noFolders }}
|
||||
</div>
|
||||
</div>
|
||||
</v-list>
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<v-menu v-model="contextMenu.show" :target="contextMenu.target as any" location="end" :close-on-content-click="true">
|
||||
<v-list density="compact">
|
||||
<v-list-item @click="openFolder">
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="small">mdi-folder-open</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ mergedLabels.contextMenu.open }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="$emit('rename-folder', contextMenu.folder)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="small">mdi-pencil</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ mergedLabels.contextMenu.rename }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="$emit('move-folder', contextMenu.folder)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="small">mdi-folder-move</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ mergedLabels.contextMenu.moveTo }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider class="my-1" />
|
||||
<v-list-item @click="$emit('delete-folder', contextMenu.folder)" class="text-error">
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="small" color="error">mdi-delete</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ mergedLabels.contextMenu.delete }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
<!-- 右键菜单 -->
|
||||
<v-menu
|
||||
v-model="contextMenu.show"
|
||||
:target="contextMenu.target as any"
|
||||
location="end"
|
||||
:close-on-content-click="true"
|
||||
>
|
||||
<v-list density="compact">
|
||||
<v-list-item @click="openFolder">
|
||||
<template #prepend>
|
||||
<v-icon size="small">
|
||||
mdi-folder-open
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ mergedLabels.contextMenu.open }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="$emit('rename-folder', contextMenu.folder)">
|
||||
<template #prepend>
|
||||
<v-icon size="small">
|
||||
mdi-pencil
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ mergedLabels.contextMenu.rename }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="$emit('move-folder', contextMenu.folder)">
|
||||
<template #prepend>
|
||||
<v-icon size="small">
|
||||
mdi-folder-move
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ mergedLabels.contextMenu.moveTo }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider class="my-1" />
|
||||
<v-list-item
|
||||
class="text-error"
|
||||
@click="$emit('delete-folder', contextMenu.folder)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
size="small"
|
||||
color="error"
|
||||
>
|
||||
mdi-delete
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ mergedLabels.contextMenu.delete }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -1,36 +1,63 @@
|
||||
<template>
|
||||
<div class="base-folder-tree-node">
|
||||
<v-list-item :active="currentFolderId === folder.folder_id" @click.stop="$emit('folder-click', folder.folder_id)"
|
||||
@contextmenu.prevent="handleContextMenu" rounded="lg" :style="{ paddingLeft: `${(depth + 1) * 16}px` }"
|
||||
:class="['folder-item', { 'drag-over': isDragOver }]"
|
||||
@dragover.prevent="handleDragOver" @dragleave="handleDragLeave" @drop.prevent="handleDrop">
|
||||
<template v-slot:prepend>
|
||||
<v-btn v-if="hasChildren" icon variant="text" size="x-small" @click.stop="toggleExpand"
|
||||
class="expand-btn">
|
||||
<v-icon size="16">{{ isExpanded ? 'mdi-chevron-down' : 'mdi-chevron-right' }}</v-icon>
|
||||
</v-btn>
|
||||
<div v-else class="expand-placeholder"></div>
|
||||
<v-icon :color="currentFolderId === folder.folder_id ? 'primary' : ''">
|
||||
{{ isExpanded ? 'mdi-folder-open' : 'mdi-folder' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title class="text-truncate">{{ folder.name }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<div class="base-folder-tree-node">
|
||||
<v-list-item
|
||||
:active="currentFolderId === folder.folder_id"
|
||||
rounded="lg"
|
||||
:style="{ paddingLeft: `${(depth + 1) * 16}px` }"
|
||||
:class="['folder-item', { 'drag-over': isDragOver }]"
|
||||
@click.stop="$emit('folder-click', folder.folder_id)"
|
||||
@contextmenu.prevent="handleContextMenu"
|
||||
@dragover.prevent="handleDragOver"
|
||||
@dragleave="handleDragLeave"
|
||||
@drop.prevent="handleDrop"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-btn
|
||||
v-if="hasChildren"
|
||||
icon
|
||||
variant="text"
|
||||
size="x-small"
|
||||
class="expand-btn"
|
||||
@click.stop="toggleExpand"
|
||||
>
|
||||
<v-icon size="16">
|
||||
{{ isExpanded ? 'mdi-chevron-down' : 'mdi-chevron-right' }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
<div
|
||||
v-else
|
||||
class="expand-placeholder"
|
||||
/>
|
||||
<v-icon :color="currentFolderId === folder.folder_id ? 'primary' : ''">
|
||||
{{ isExpanded ? 'mdi-folder-open' : 'mdi-folder' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title class="text-truncate">
|
||||
{{ folder.name }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<!-- 子文件夹 -->
|
||||
<v-expand-transition>
|
||||
<div v-show="isExpanded && hasChildren">
|
||||
<BaseFolderTreeNode v-for="child in folder.children" :key="child.folder_id" :folder="child" :depth="depth + 1"
|
||||
:current-folder-id="currentFolderId" :search-query="searchQuery"
|
||||
:expanded-folder-ids="expandedFolderIds" :accept-drop-types="acceptDropTypes"
|
||||
@folder-click="$emit('folder-click', $event)"
|
||||
@folder-context-menu="$emit('folder-context-menu', $event)"
|
||||
@item-dropped="$emit('item-dropped', $event)"
|
||||
@toggle-expansion="$emit('toggle-expansion', $event)"
|
||||
@set-expansion="$emit('set-expansion', $event)" />
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
</div>
|
||||
<!-- 子文件夹 -->
|
||||
<v-expand-transition>
|
||||
<div v-show="isExpanded && hasChildren">
|
||||
<BaseFolderTreeNode
|
||||
v-for="child in folder.children"
|
||||
:key="child.folder_id"
|
||||
:folder="child"
|
||||
:depth="depth + 1"
|
||||
:current-folder-id="currentFolderId"
|
||||
:search-query="searchQuery"
|
||||
:expanded-folder-ids="expandedFolderIds"
|
||||
:accept-drop-types="acceptDropTypes"
|
||||
@folder-click="$emit('folder-click', $event)"
|
||||
@folder-context-menu="$emit('folder-context-menu', $event)"
|
||||
@item-dropped="$emit('item-dropped', $event)"
|
||||
@toggle-expansion="$emit('toggle-expansion', $event)"
|
||||
@set-expansion="$emit('set-expansion', $event)"
|
||||
/>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -1,30 +1,55 @@
|
||||
<template>
|
||||
<div class="base-move-target-node">
|
||||
<v-list-item :active="selectedFolderId === folder.folder_id" :disabled="isDisabled"
|
||||
@click.stop="!isDisabled && $emit('select', folder.folder_id)" rounded="lg"
|
||||
:style="{ paddingLeft: `${(depth + 1) * 16}px` }" class="folder-item">
|
||||
<template v-slot:prepend>
|
||||
<v-btn v-if="hasChildren" icon variant="text" size="x-small" @click.stop="toggleExpand"
|
||||
class="expand-btn" :disabled="isDisabled">
|
||||
<v-icon size="16">{{ isExpanded ? 'mdi-chevron-down' : 'mdi-chevron-right' }}</v-icon>
|
||||
</v-btn>
|
||||
<div v-else class="expand-placeholder"></div>
|
||||
<v-icon :color="isDisabled ? 'grey' : (selectedFolderId === folder.folder_id ? 'primary' : '')">
|
||||
{{ isExpanded ? 'mdi-folder-open' : 'mdi-folder' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title class="text-truncate">{{ folder.name }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<div class="base-move-target-node">
|
||||
<v-list-item
|
||||
:active="selectedFolderId === folder.folder_id"
|
||||
:disabled="isDisabled"
|
||||
rounded="lg"
|
||||
:style="{ paddingLeft: `${(depth + 1) * 16}px` }"
|
||||
class="folder-item"
|
||||
@click.stop="!isDisabled && $emit('select', folder.folder_id)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-btn
|
||||
v-if="hasChildren"
|
||||
icon
|
||||
variant="text"
|
||||
size="x-small"
|
||||
class="expand-btn"
|
||||
:disabled="isDisabled"
|
||||
@click.stop="toggleExpand"
|
||||
>
|
||||
<v-icon size="16">
|
||||
{{ isExpanded ? 'mdi-chevron-down' : 'mdi-chevron-right' }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
<div
|
||||
v-else
|
||||
class="expand-placeholder"
|
||||
/>
|
||||
<v-icon :color="isDisabled ? 'grey' : (selectedFolderId === folder.folder_id ? 'primary' : '')">
|
||||
{{ isExpanded ? 'mdi-folder-open' : 'mdi-folder' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title class="text-truncate">
|
||||
{{ folder.name }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<!-- 子文件夹 -->
|
||||
<v-expand-transition>
|
||||
<div v-show="isExpanded && hasChildren">
|
||||
<BaseMoveTargetNode v-for="child in folder.children" :key="child.folder_id" :folder="child" :depth="depth + 1"
|
||||
:selected-folder-id="selectedFolderId" :disabled-folder-ids="disabledFolderIds"
|
||||
@select="$emit('select', $event)" />
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
</div>
|
||||
<!-- 子文件夹 -->
|
||||
<v-expand-transition>
|
||||
<div v-show="isExpanded && hasChildren">
|
||||
<BaseMoveTargetNode
|
||||
v-for="child in folder.children"
|
||||
:key="child.folder_id"
|
||||
:folder="child"
|
||||
:depth="depth + 1"
|
||||
:selected-folder-id="selectedFolderId"
|
||||
:disabled-folder-ids="disabledFolderIds"
|
||||
@select="$emit('select', $event)"
|
||||
/>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -1,52 +1,86 @@
|
||||
<template>
|
||||
<v-dialog v-model="showDialog" max-width="500px" persistent>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<v-icon class="mr-2">mdi-folder-move</v-icon>
|
||||
{{ labels.title }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<p class="text-body-2 text-medium-emphasis mb-4">
|
||||
{{ labels.description }}
|
||||
</p>
|
||||
<v-dialog
|
||||
v-model="showDialog"
|
||||
max-width="500px"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<v-icon class="mr-2">
|
||||
mdi-folder-move
|
||||
</v-icon>
|
||||
{{ labels.title }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<p class="text-body-2 text-medium-emphasis mb-4">
|
||||
{{ labels.description }}
|
||||
</p>
|
||||
|
||||
<!-- 文件夹选择树 -->
|
||||
<div class="folder-select-tree">
|
||||
<v-list density="compact" nav class="tree-list">
|
||||
<!-- 根目录选项 -->
|
||||
<v-list-item :active="selectedFolderId === null" @click="selectFolder(null)" rounded="lg"
|
||||
class="mb-1">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-home</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ labels.rootFolder }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<!-- 文件夹选择树 -->
|
||||
<div class="folder-select-tree">
|
||||
<v-list
|
||||
density="compact"
|
||||
nav
|
||||
class="tree-list"
|
||||
>
|
||||
<!-- 根目录选项 -->
|
||||
<v-list-item
|
||||
:active="selectedFolderId === null"
|
||||
rounded="lg"
|
||||
class="mb-1"
|
||||
@click="selectFolder(null)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-home</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ labels.rootFolder }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<!-- 文件夹树 -->
|
||||
<template v-if="!treeLoading">
|
||||
<BaseMoveTargetNode v-for="folder in folderTree" :key="folder.folder_id" :folder="folder"
|
||||
:depth="0" :selected-folder-id="selectedFolderId" :disabled-folder-ids="disabledFolderIds"
|
||||
@select="selectFolder" />
|
||||
</template>
|
||||
<!-- 文件夹树 -->
|
||||
<template v-if="!treeLoading">
|
||||
<BaseMoveTargetNode
|
||||
v-for="folder in folderTree"
|
||||
:key="folder.folder_id"
|
||||
:folder="folder"
|
||||
:depth="0"
|
||||
:selected-folder-id="selectedFolderId"
|
||||
:disabled-folder-ids="disabledFolderIds"
|
||||
@select="selectFolder"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="treeLoading" class="text-center pa-4">
|
||||
<v-progress-circular indeterminate size="24" />
|
||||
</div>
|
||||
</v-list>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="closeDialog">
|
||||
{{ labels.cancelButton }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" variant="flat" @click="submitMove" :loading="loading">
|
||||
{{ labels.moveButton }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<!-- 加载状态 -->
|
||||
<div
|
||||
v-if="treeLoading"
|
||||
class="text-center pa-4"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
size="24"
|
||||
/>
|
||||
</div>
|
||||
</v-list>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="closeDialog"
|
||||
>
|
||||
{{ labels.cancelButton }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="flat"
|
||||
:loading="loading"
|
||||
@click="submitMove"
|
||||
>
|
||||
{{ labels.moveButton }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
<template>
|
||||
<v-dialog v-model="showDialog" max-width="800px" max-height="90%" @after-enter="prepareData">
|
||||
<v-dialog
|
||||
v-model="showDialog"
|
||||
max-width="800px"
|
||||
max-height="90%"
|
||||
@after-enter="prepareData"
|
||||
>
|
||||
<v-card
|
||||
:title="updatingMode ? `${tm('dialog.edit')} ${updatingPlatformConfig.id} ${tm('dialog.adapter')}` : tm('dialog.addPlatform')">
|
||||
<v-card-text ref="dialogScrollContainer" class="pa-4 ml-2" style="overflow-y: auto;">
|
||||
<div class="d-flex align-start" style="width: 100%;">
|
||||
:title="updatingMode ? `${tm('dialog.edit')} ${updatingPlatformConfig.id} ${tm('dialog.adapter')}` : tm('dialog.addPlatform')"
|
||||
>
|
||||
<v-card-text
|
||||
ref="dialogScrollContainer"
|
||||
class="pa-4 ml-2"
|
||||
style="overflow-y: auto;"
|
||||
>
|
||||
<div
|
||||
class="d-flex align-start"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<div>
|
||||
<v-icon icon="mdi-numeric-1-circle" class="mr-3"></v-icon>
|
||||
<v-icon
|
||||
icon="mdi-numeric-1-circle"
|
||||
class="mr-3"
|
||||
/>
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<h3>
|
||||
@@ -13,52 +29,87 @@
|
||||
</h3>
|
||||
<small style="color: grey;">{{ tm('createDialog.step1Hint') }}</small>
|
||||
<div>
|
||||
|
||||
<div v-if="!updatingMode">
|
||||
<v-select v-model="selectedPlatformType" :items="Object.keys(platformTemplates)" item-title="name"
|
||||
item-value="name" :label="tm('createDialog.platformTypeLabel')" variant="outlined" rounded="md" dense hide-details class="mt-6"
|
||||
style="max-width: 30%; min-width: 300px;">
|
||||
|
||||
<template v-slot:item="{ props: itemProps, item }">
|
||||
<v-select
|
||||
v-model="selectedPlatformType"
|
||||
:items="Object.keys(platformTemplates)"
|
||||
item-title="name"
|
||||
item-value="name"
|
||||
:label="tm('createDialog.platformTypeLabel')"
|
||||
variant="outlined"
|
||||
rounded="md"
|
||||
dense
|
||||
hide-details
|
||||
class="mt-6"
|
||||
style="max-width: 30%; min-width: 300px;"
|
||||
>
|
||||
<template #item="{ props: itemProps, item }">
|
||||
<v-list-item v-bind="itemProps">
|
||||
<template v-slot:prepend>
|
||||
<img :src="getPlatformIcon(platformTemplates[item.raw].type)"
|
||||
style="width: 32px; height: 32px; object-fit: contain; margin-right: 16px;" />
|
||||
<template #prepend>
|
||||
<img
|
||||
:src="getPlatformIcon(platformTemplates[item.raw].type)"
|
||||
style="width: 32px; height: 32px; object-fit: contain; margin-right: 16px;"
|
||||
>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
</v-select>
|
||||
<div class="mt-3" v-if="selectedPlatformConfig">
|
||||
<v-btn color="info" variant="tonal" @click="openTutorial" class="mt-2">
|
||||
<v-icon start>mdi-book-open-variant</v-icon>
|
||||
<div
|
||||
v-if="selectedPlatformConfig"
|
||||
class="mt-3"
|
||||
>
|
||||
<v-btn
|
||||
color="info"
|
||||
variant="tonal"
|
||||
class="mt-2"
|
||||
@click="openTutorial"
|
||||
>
|
||||
<v-icon start>
|
||||
mdi-book-open-variant
|
||||
</v-icon>
|
||||
{{ tm('dialog.viewTutorial') }}
|
||||
</v-btn>
|
||||
<div class="mt-2">
|
||||
<AstrBotConfig :iterable="selectedPlatformConfig" :metadata="metadata['platform_group']?.metadata"
|
||||
metadataKey="platform" />
|
||||
<AstrBotConfig
|
||||
:iterable="selectedPlatformConfig"
|
||||
:metadata="metadata['platform_group']?.metadata"
|
||||
metadata-key="platform"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<v-text-field :label="tm('createDialog.platformTypeLabel')" variant="outlined" rounded="md" dense hide-details class="mt-6"
|
||||
style="max-width: 30%; min-width: 300px;" v-model="updatingPlatformConfig.type"
|
||||
disabled></v-text-field>
|
||||
<v-text-field
|
||||
:model-value="updatingPlatformConfig.type"
|
||||
:label="tm('createDialog.platformTypeLabel')"
|
||||
variant="outlined"
|
||||
rounded="md"
|
||||
dense
|
||||
hide-details
|
||||
class="mt-6"
|
||||
style="max-width: 30%; min-width: 300px;"
|
||||
disabled
|
||||
/>
|
||||
<div class="mt-3">
|
||||
<div class="mt-2">
|
||||
<AstrBotConfig :iterable="updatingPlatformConfig" :metadata="metadata['platform_group']?.metadata"
|
||||
metadataKey="platform" />
|
||||
<AstrBotConfig
|
||||
:iterable="updatingPlatformConfig"
|
||||
:metadata="metadata['platform_group']?.metadata"
|
||||
metadata-key="platform"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-start mt-6">
|
||||
<div>
|
||||
<v-icon icon="mdi-numeric-2-circle" class="mr-3"></v-icon>
|
||||
<v-icon
|
||||
icon="mdi-numeric-2-circle"
|
||||
class="mr-3"
|
||||
/>
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
@@ -67,46 +118,93 @@
|
||||
<h3>
|
||||
{{ tm('createDialog.configFileTitle') }}
|
||||
</h3>
|
||||
<v-chip size="x-small" color="primary" variant="tonal" rounded="sm" class="ml-2"
|
||||
v-if="!updatingMode">{{ tm('createDialog.optional') }}</v-chip>
|
||||
<v-chip
|
||||
v-if="!updatingMode"
|
||||
size="x-small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
rounded="sm"
|
||||
class="ml-2"
|
||||
>
|
||||
{{ tm('createDialog.optional') }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<small style="color: grey;">{{ tm('createDialog.configHint') }}</small>
|
||||
<small style="color: grey;" v-if="!updatingMode">{{ tm('createDialog.configDefaultHint') }}</small>
|
||||
<small
|
||||
v-if="!updatingMode"
|
||||
style="color: grey;"
|
||||
>{{ tm('createDialog.configDefaultHint') }}</small>
|
||||
</div>
|
||||
<div>
|
||||
<v-btn variant="plain" icon @click="toggleConfigSection" class="mt-2">
|
||||
<v-btn
|
||||
variant="plain"
|
||||
icon
|
||||
class="mt-2"
|
||||
@click="toggleConfigSection"
|
||||
>
|
||||
<v-icon>{{ showConfigSection ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-if="showConfigSection">
|
||||
<div v-if="!updatingMode">
|
||||
<v-radio-group class="mt-2" v-model="aBConfigRadioVal" hide-details="true">
|
||||
<v-radio-group
|
||||
v-model="aBConfigRadioVal"
|
||||
class="mt-2"
|
||||
hide-details="true"
|
||||
>
|
||||
<v-radio value="0">
|
||||
<template v-slot:label>
|
||||
<template #label>
|
||||
<span>{{ tm('createDialog.useExistingConfig') }}</span>
|
||||
</template>
|
||||
</v-radio>
|
||||
<div class="d-flex align-center ml-10 my-2" v-if="aBConfigRadioVal === '0'">
|
||||
<v-select v-model="selectedAbConfId" :items="configInfoList" item-title="name"
|
||||
item-value="id" :label="tm('createDialog.selectConfigLabel')" variant="outlined" rounded="md" dense hide-details
|
||||
style="max-width: 30%; min-width: 200px;">
|
||||
</v-select>
|
||||
<v-btn icon variant="text" density="comfortable" class="ml-2"
|
||||
:disabled="!selectedAbConfId" @click="openConfigDrawer(selectedAbConfId)">
|
||||
<div
|
||||
v-if="aBConfigRadioVal === '0'"
|
||||
class="d-flex align-center ml-10 my-2"
|
||||
>
|
||||
<v-select
|
||||
v-model="selectedAbConfId"
|
||||
:items="configInfoList"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
:label="tm('createDialog.selectConfigLabel')"
|
||||
variant="outlined"
|
||||
rounded="md"
|
||||
dense
|
||||
hide-details
|
||||
style="max-width: 30%; min-width: 200px;"
|
||||
/>
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
density="comfortable"
|
||||
class="ml-2"
|
||||
:disabled="!selectedAbConfId"
|
||||
@click="openConfigDrawer(selectedAbConfId)"
|
||||
>
|
||||
<v-icon>mdi-arrow-top-right-thick</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-radio value="1" :label="tm('createDialog.createNewConfig')">
|
||||
</v-radio>
|
||||
<div class="d-flex align-center" v-if="aBConfigRadioVal === '1'">
|
||||
<v-text-field v-model="selectedAbConfId" :label="tm('createDialog.newConfigNameLabel')" variant="outlined" rounded="md" dense
|
||||
hide-details style="max-width: 30%; min-width: 200px;" class="ml-10 my-2">
|
||||
</v-text-field>
|
||||
<v-radio
|
||||
value="1"
|
||||
:label="tm('createDialog.createNewConfig')"
|
||||
/>
|
||||
<div
|
||||
v-if="aBConfigRadioVal === '1'"
|
||||
class="d-flex align-center"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="selectedAbConfId"
|
||||
:label="tm('createDialog.newConfigNameLabel')"
|
||||
variant="outlined"
|
||||
rounded="md"
|
||||
dense
|
||||
hide-details
|
||||
style="max-width: 30%; min-width: 200px;"
|
||||
class="ml-10 my-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</v-radio-group>
|
||||
|
||||
<!-- 现有配置文件预览区域 -->
|
||||
@@ -126,132 +224,268 @@
|
||||
</div> -->
|
||||
|
||||
<!-- 新配置文件编辑区域 -->
|
||||
<div v-if="aBConfigRadioVal === '1'" class="mt-4">
|
||||
<div v-if="newConfigLoading" class="d-flex justify-center py-4">
|
||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||
<div
|
||||
v-if="aBConfigRadioVal === '1'"
|
||||
class="mt-4"
|
||||
>
|
||||
<div
|
||||
v-if="newConfigLoading"
|
||||
class="d-flex justify-center py-4"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="newConfigData && newConfigMetadata" class="config-preview-container">
|
||||
<h4 class="mb-3">{{ tm('createDialog.newConfigTitle') }}</h4>
|
||||
<AstrBotCoreConfigWrapper :metadata="newConfigMetadata" :config_data="newConfigData" />
|
||||
<div
|
||||
v-else-if="newConfigData && newConfigMetadata"
|
||||
class="config-preview-container"
|
||||
>
|
||||
<h4 class="mb-3">
|
||||
{{ tm('createDialog.newConfigTitle') }}
|
||||
</h4>
|
||||
<AstrBotCoreConfigWrapper
|
||||
:metadata="newConfigMetadata"
|
||||
:config_data="newConfigData"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="text-center py-4 text-grey">
|
||||
<div
|
||||
v-else
|
||||
class="text-center py-4 text-grey"
|
||||
>
|
||||
<v-icon>mdi-information-outline</v-icon>
|
||||
<p class="mt-2">{{ tm('createDialog.newConfigLoadFailed') }}</p>
|
||||
<p class="mt-2">
|
||||
{{ tm('createDialog.newConfigLoadFailed') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="mb-3 d-flex align-center justify-space-between">
|
||||
<div>
|
||||
<v-btn v-if="isEditingRoutes" color="primary" variant="tonal" @click="addNewRoute" size="small">
|
||||
<v-icon start>mdi-plus</v-icon>
|
||||
<v-btn
|
||||
v-if="isEditingRoutes"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
@click="addNewRoute"
|
||||
>
|
||||
<v-icon start>
|
||||
mdi-plus
|
||||
</v-icon>
|
||||
{{ tm('createDialog.addRouteRule') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-btn :color="isEditingRoutes ? 'grey' : 'primary'" variant="tonal" size="small"
|
||||
@click="toggleEditMode">
|
||||
<v-icon start>{{ isEditingRoutes ? 'mdi-eye' : 'mdi-pencil' }}</v-icon>
|
||||
<v-btn
|
||||
:color="isEditingRoutes ? 'grey' : 'primary'"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
@click="toggleEditMode"
|
||||
>
|
||||
<v-icon start>
|
||||
{{ isEditingRoutes ? 'mdi-eye' : 'mdi-pencil' }}
|
||||
</v-icon>
|
||||
{{ isEditingRoutes ? tm('createDialog.viewMode') : tm('createDialog.editMode') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-data-table :headers="routeTableHeaders" :items="platformRoutes" item-value="umop"
|
||||
:no-data-text="tm('createDialog.noRouteRules')" hide-default-footer :items-per-page="-1" class="mt-2"
|
||||
variant="outlined">
|
||||
|
||||
<template v-slot:item.source="{ item }">
|
||||
<div class="d-flex align-center" style="min-width: 250px;">
|
||||
<v-select v-if="isEditingRoutes" v-model="item.messageType" :items="messageTypeOptions"
|
||||
item-title="label" item-value="value" variant="outlined" density="compact" hide-details
|
||||
style="max-width: 140px;">
|
||||
</v-select>
|
||||
<v-data-table
|
||||
:headers="routeTableHeaders"
|
||||
:items="platformRoutes"
|
||||
item-value="umop"
|
||||
:no-data-text="tm('createDialog.noRouteRules')"
|
||||
hide-default-footer
|
||||
:items-per-page="-1"
|
||||
class="mt-2"
|
||||
variant="outlined"
|
||||
>
|
||||
<template #item.source="{ item }">
|
||||
<div
|
||||
class="d-flex align-center"
|
||||
style="min-width: 250px;"
|
||||
>
|
||||
<v-select
|
||||
v-if="isEditingRoutes"
|
||||
v-model="item.messageType"
|
||||
:items="messageTypeOptions"
|
||||
item-title="label"
|
||||
item-value="value"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
style="max-width: 140px;"
|
||||
/>
|
||||
<small v-else>{{ getMessageTypeLabel(item.messageType) }}</small>
|
||||
<small class="mx-1">:</small>
|
||||
<v-text-field v-if="isEditingRoutes" v-model="item.sessionId" variant="outlined" density="compact"
|
||||
hide-details :placeholder="tm('createDialog.sessionIdPlaceholder')">
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
v-if="isEditingRoutes"
|
||||
v-model="item.sessionId"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
:placeholder="tm('createDialog.sessionIdPlaceholder')"
|
||||
/>
|
||||
<small v-else>{{ item.sessionId === '*' ? tm('createDialog.allSessions') : item.sessionId }}</small>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.configId="{ item }">
|
||||
<template #item.configId="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<v-select v-if="isEditingRoutes" v-model="item.configId" :items="configInfoList"
|
||||
item-title="name" item-value="id" variant="outlined" density="compact"
|
||||
style="min-width: 200px;" hide-details>
|
||||
</v-select>
|
||||
<v-select
|
||||
v-if="isEditingRoutes"
|
||||
v-model="item.configId"
|
||||
:items="configInfoList"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
style="min-width: 200px;"
|
||||
hide-details
|
||||
/>
|
||||
<div v-else>
|
||||
<small>{{ getConfigName(item.configId) }}</small>
|
||||
</div>
|
||||
<v-btn icon variant="text" density="compact" class="ml-2"
|
||||
:disabled="!item.configId" @click="openConfigDrawer(item.configId)">
|
||||
<v-icon size="18">mdi-arrow-top-right-thick</v-icon>
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
density="compact"
|
||||
class="ml-2"
|
||||
:disabled="!item.configId"
|
||||
@click="openConfigDrawer(item.configId)"
|
||||
>
|
||||
<v-icon size="18">
|
||||
mdi-arrow-top-right-thick
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<small v-if="configInfoList.findIndex(c => c.id === item.configId) === -1" style="color: red;"
|
||||
class="ml-2">{{ tm('createDialog.configMissing') }}</small>
|
||||
<small
|
||||
v-if="configInfoList.findIndex(c => c.id === item.configId) === -1"
|
||||
style="color: red;"
|
||||
class="ml-2"
|
||||
>{{ tm('createDialog.configMissing') }}</small>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item, index }">
|
||||
<div v-if="isEditingRoutes" class="d-flex align-center">
|
||||
<v-btn icon size="x-small" variant="text" @click="moveRouteUp(index)" :disabled="index === 0">
|
||||
<template #item.actions="{ item, index }">
|
||||
<div
|
||||
v-if="isEditingRoutes"
|
||||
class="d-flex align-center"
|
||||
>
|
||||
<v-btn
|
||||
icon
|
||||
size="x-small"
|
||||
variant="text"
|
||||
:disabled="index === 0"
|
||||
@click="moveRouteUp(index)"
|
||||
>
|
||||
<v-icon>mdi-arrow-up</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon size="x-small" variant="text" @click="moveRouteDown(index)"
|
||||
:disabled="index === platformRoutes.length - 1">
|
||||
<v-btn
|
||||
icon
|
||||
size="x-small"
|
||||
variant="text"
|
||||
:disabled="index === platformRoutes.length - 1"
|
||||
@click="moveRouteDown(index)"
|
||||
>
|
||||
<v-icon>mdi-arrow-down</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon size="x-small" variant="text" color="error" @click="deleteRoute(index)">
|
||||
<v-btn
|
||||
icon
|
||||
size="x-small"
|
||||
variant="text"
|
||||
color="error"
|
||||
@click="deleteRoute(index)"
|
||||
>
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<span v-else class="text-grey">-</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-grey"
|
||||
>-</span>
|
||||
</template>
|
||||
|
||||
</v-data-table>
|
||||
<small class="ml-2 mt-2 d-block" style="color: grey">{{ tm('createDialog.routeHint') }}</small>
|
||||
<small
|
||||
class="ml-2 mt-2 d-block"
|
||||
style="color: grey"
|
||||
>{{ tm('createDialog.routeHint') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text @click="closeDialog">{{ tm('dialog.cancel') }}</v-btn>
|
||||
<v-btn :disabled="!canSave" color="primary" v-if="!updatingMode" @click="newPlatform" :loading="loading">{{
|
||||
tm('dialog.save') }}</v-btn>
|
||||
<v-btn :disabled="!selectedAbConfId" color="primary" v-else @click="newPlatform" :loading="loading">{{
|
||||
tm('dialog.save') }}</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
@click="closeDialog"
|
||||
>
|
||||
{{ tm('dialog.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="!updatingMode"
|
||||
:disabled="!canSave"
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
@click="newPlatform"
|
||||
>
|
||||
{{
|
||||
tm('dialog.save') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
:disabled="!selectedAbConfId"
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
@click="newPlatform"
|
||||
>
|
||||
{{
|
||||
tm('dialog.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- ID冲突确认对话框 -->
|
||||
<v-dialog v-model="showIdConflictDialog" max-width="450" persistent>
|
||||
<v-dialog
|
||||
v-model="showIdConflictDialog"
|
||||
max-width="450"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="text-h6 bg-warning d-flex align-center">
|
||||
<v-icon start class="me-2">mdi-alert-circle-outline</v-icon>
|
||||
<v-icon
|
||||
start
|
||||
class="me-2"
|
||||
>
|
||||
mdi-alert-circle-outline
|
||||
</v-icon>
|
||||
{{ tm('dialog.idConflict.title') }}
|
||||
</v-card-title>
|
||||
<v-card-text class="py-4 text-body-1 text-medium-emphasis">
|
||||
{{ tm('dialog.idConflict.message', { id: conflictId }) }}
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" variant="text" @click="handleIdConflictConfirm(false)">{{ tm('dialog.idConflict.confirm')
|
||||
}}</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="grey"
|
||||
variant="text"
|
||||
@click="handleIdConflictConfirm(false)"
|
||||
>
|
||||
{{ tm('dialog.idConflict.confirm')
|
||||
}}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 安全警告对话框 -->
|
||||
<v-dialog v-model="showOneBotEmptyTokenWarnDialog" max-width="600" persistent>
|
||||
<v-dialog
|
||||
v-model="showOneBotEmptyTokenWarnDialog"
|
||||
max-width="600"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
{{ tm('dialog.securityWarning.title') }}
|
||||
@@ -259,15 +493,22 @@
|
||||
<v-card-text class="py-4">
|
||||
<p>{{ tm('dialog.securityWarning.aiocqhttpTokenMissing') }}</p>
|
||||
<span><a
|
||||
href="https://docs.astrbot.app/deploy/platform/aiocqhttp/napcat.html#%E9%99%84%E5%BD%95-%E5%A2%9E%E5%BC%BA%E8%BF%9E%E6%8E%A5%E5%AE%89%E5%85%A8%E6%80%A7"
|
||||
target="_blank">{{ tm('dialog.securityWarning.learnMore') }}</a></span>
|
||||
href="https://docs.astrbot.app/deploy/platform/aiocqhttp/napcat.html#%E9%99%84%E5%BD%95-%E5%A2%9E%E5%BC%BA%E8%BF%9E%E6%8E%A5%E5%AE%89%E5%85%A8%E6%80%A7"
|
||||
target="_blank"
|
||||
>{{ tm('dialog.securityWarning.learnMore') }}</a></span>
|
||||
</v-card-text>
|
||||
<v-card-actions class="px-4 pb-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="error" @click="handleOneBotEmptyTokenWarningDismiss(true)">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="error"
|
||||
@click="handleOneBotEmptyTokenWarningDismiss(true)"
|
||||
>
|
||||
{{ tm('createDialog.warningContinue') }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" @click="handleOneBotEmptyTokenWarningDismiss(false)">
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="handleOneBotEmptyTokenWarningDismiss(false)"
|
||||
>
|
||||
{{ tm('createDialog.warningEditAgain') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
@@ -282,21 +523,34 @@
|
||||
:scrim="true"
|
||||
@click:outside="closeConfigDrawer"
|
||||
>
|
||||
<v-card class="config-drawer-card" elevation="12">
|
||||
<v-card
|
||||
class="config-drawer-card"
|
||||
elevation="12"
|
||||
>
|
||||
<div class="config-drawer-header">
|
||||
<div>
|
||||
<span class="text-h6">{{ tm('createDialog.configDrawerTitle') }}</span>
|
||||
<div v-if="configDrawerTargetId" class="text-caption text-grey">
|
||||
<div
|
||||
v-if="configDrawerTargetId"
|
||||
class="text-caption text-grey"
|
||||
>
|
||||
{{ tm('createDialog.configDrawerIdLabel') }}: {{ configDrawerTargetId }}
|
||||
</div>
|
||||
</div>
|
||||
<v-btn icon variant="text" @click="closeConfigDrawer">
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
@click="closeConfigDrawer"
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-divider></v-divider>
|
||||
<v-divider />
|
||||
<div class="config-drawer-content">
|
||||
<ConfigPage v-if="showConfigDrawer" :initial-config-id="configDrawerTargetId" />
|
||||
<ConfigPage
|
||||
v-if="showConfigDrawer"
|
||||
:initial-config-id="configDrawerTargetId"
|
||||
/>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-overlay>
|
||||
@@ -314,7 +568,6 @@ import ConfigPage from '@/views/ConfigPage.vue';
|
||||
export default {
|
||||
name: 'AddNewPlatform',
|
||||
components: { AstrBotConfig, AstrBotCoreConfigWrapper, ConfigPage },
|
||||
emits: ['update:show', 'show-toast', 'refresh-config'],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
@@ -337,6 +590,11 @@ export default {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
emits: ['update:show', 'show-toast', 'refresh-config'],
|
||||
setup() {
|
||||
const { tm } = useModuleI18n('features/platform');
|
||||
return { tm };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedPlatformType: null,
|
||||
@@ -384,10 +642,6 @@ export default {
|
||||
originalUpdatingPlatformId: null,
|
||||
};
|
||||
},
|
||||
setup() {
|
||||
const { tm } = useModuleI18n('features/platform');
|
||||
return { tm };
|
||||
},
|
||||
computed: {
|
||||
showDialog: {
|
||||
get() {
|
||||
|
||||
@@ -1,72 +1,138 @@
|
||||
<template>
|
||||
<v-dialog v-model="showDialog" max-width="1100px" min-height="95%">
|
||||
<v-card :title="tm('dialogs.addProvider.title')">
|
||||
<v-card-text style="overflow-y: auto;">
|
||||
<v-tabs v-model="activeProviderTab" grow>
|
||||
<v-tab value="agent_runner" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-cogs</v-icon>
|
||||
{{ tm('dialogs.addProvider.tabs.agentRunner') }}
|
||||
</v-tab>
|
||||
<v-tab value="speech_to_text" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-microphone-message</v-icon>
|
||||
{{ tm('dialogs.addProvider.tabs.speechToText') }}
|
||||
</v-tab>
|
||||
<v-tab value="text_to_speech" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-volume-high</v-icon>
|
||||
{{ tm('dialogs.addProvider.tabs.textToSpeech') }}
|
||||
</v-tab>
|
||||
<v-tab value="embedding" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-code-json</v-icon>
|
||||
{{ tm('dialogs.addProvider.tabs.embedding') }}
|
||||
</v-tab>
|
||||
<v-tab value="rerank" class="font-weight-medium px-3">
|
||||
<v-icon start>mdi-compare-vertical</v-icon>
|
||||
{{ tm('dialogs.addProvider.tabs.rerank') }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<v-dialog
|
||||
v-model="showDialog"
|
||||
max-width="1100px"
|
||||
min-height="95%"
|
||||
>
|
||||
<v-card :title="tm('dialogs.addProvider.title')">
|
||||
<v-card-text style="overflow-y: auto;">
|
||||
<v-tabs
|
||||
v-model="activeProviderTab"
|
||||
grow
|
||||
>
|
||||
<v-tab
|
||||
value="agent_runner"
|
||||
class="font-weight-medium px-3"
|
||||
>
|
||||
<v-icon start>
|
||||
mdi-cogs
|
||||
</v-icon>
|
||||
{{ tm('dialogs.addProvider.tabs.agentRunner') }}
|
||||
</v-tab>
|
||||
<v-tab
|
||||
value="speech_to_text"
|
||||
class="font-weight-medium px-3"
|
||||
>
|
||||
<v-icon start>
|
||||
mdi-microphone-message
|
||||
</v-icon>
|
||||
{{ tm('dialogs.addProvider.tabs.speechToText') }}
|
||||
</v-tab>
|
||||
<v-tab
|
||||
value="text_to_speech"
|
||||
class="font-weight-medium px-3"
|
||||
>
|
||||
<v-icon start>
|
||||
mdi-volume-high
|
||||
</v-icon>
|
||||
{{ tm('dialogs.addProvider.tabs.textToSpeech') }}
|
||||
</v-tab>
|
||||
<v-tab
|
||||
value="embedding"
|
||||
class="font-weight-medium px-3"
|
||||
>
|
||||
<v-icon start>
|
||||
mdi-code-json
|
||||
</v-icon>
|
||||
{{ tm('dialogs.addProvider.tabs.embedding') }}
|
||||
</v-tab>
|
||||
<v-tab
|
||||
value="rerank"
|
||||
class="font-weight-medium px-3"
|
||||
>
|
||||
<v-icon start>
|
||||
mdi-compare-vertical
|
||||
</v-icon>
|
||||
{{ tm('dialogs.addProvider.tabs.rerank') }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-window v-model="activeProviderTab" class="mt-4">
|
||||
<v-window-item
|
||||
v-for="tabType in ['chat_completion', 'agent_runner', 'speech_to_text', 'text_to_speech', 'embedding', 'rerank']"
|
||||
:key="tabType" :value="tabType">
|
||||
<v-row class="mt-1">
|
||||
<v-col v-for="(template, name) in getTemplatesByType(tabType)" :key="name" cols="12" sm="6"
|
||||
md="4">
|
||||
<v-card variant="outlined" hover class="provider-card"
|
||||
@click="selectProviderTemplate(name)">
|
||||
<div class="provider-card-content">
|
||||
<div class="provider-card-text">
|
||||
<v-card-title class="provider-card-title">{{ name }}</v-card-title>
|
||||
<v-card-text
|
||||
class="text-caption text-medium-emphasis provider-card-description">
|
||||
{{ getProviderDescription(template, name) }}
|
||||
</v-card-text>
|
||||
</div>
|
||||
<div class="provider-card-logo">
|
||||
<img :src="getProviderIcon(template.provider)"
|
||||
v-if="getProviderIcon(template.provider)" class="provider-logo-img">
|
||||
<div v-else class="provider-logo-fallback">
|
||||
{{ name[0].toUpperCase() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col v-if="Object.keys(getTemplatesByType(tabType)).length === 0" cols="12">
|
||||
<v-alert type="info" variant="tonal">
|
||||
{{ tm('dialogs.addProvider.noTemplates') }}
|
||||
</v-alert>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text @click="closeDialog">{{ tm('dialogs.config.cancel') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-window
|
||||
v-model="activeProviderTab"
|
||||
class="mt-4"
|
||||
>
|
||||
<v-window-item
|
||||
v-for="tabType in ['chat_completion', 'agent_runner', 'speech_to_text', 'text_to_speech', 'embedding', 'rerank']"
|
||||
:key="tabType"
|
||||
:value="tabType"
|
||||
>
|
||||
<v-row class="mt-1">
|
||||
<v-col
|
||||
v-for="(template, name) in getTemplatesByType(tabType)"
|
||||
:key="name"
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
>
|
||||
<v-card
|
||||
variant="outlined"
|
||||
hover
|
||||
class="provider-card"
|
||||
@click="selectProviderTemplate(name)"
|
||||
>
|
||||
<div class="provider-card-content">
|
||||
<div class="provider-card-text">
|
||||
<v-card-title class="provider-card-title">
|
||||
{{ name }}
|
||||
</v-card-title>
|
||||
<v-card-text
|
||||
class="text-caption text-medium-emphasis provider-card-description"
|
||||
>
|
||||
{{ getProviderDescription(template, name) }}
|
||||
</v-card-text>
|
||||
</div>
|
||||
<div class="provider-card-logo">
|
||||
<img
|
||||
v-if="getProviderIcon(template.provider)"
|
||||
:src="getProviderIcon(template.provider)"
|
||||
class="provider-logo-img"
|
||||
>
|
||||
<div
|
||||
v-else
|
||||
class="provider-logo-fallback"
|
||||
>
|
||||
{{ name[0].toUpperCase() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col
|
||||
v-if="Object.keys(getTemplatesByType(tabType)).length === 0"
|
||||
cols="12"
|
||||
>
|
||||
<v-alert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ tm('dialogs.addProvider.noTemplates') }}
|
||||
</v-alert>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
@click="closeDialog"
|
||||
>
|
||||
{{ tm('dialogs.config.cancel') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
<template>
|
||||
<div class="mt-4">
|
||||
<div class="d-flex align-center ga-2 mb-2">
|
||||
<h3 class="text-h5 font-weight-bold mb-0">{{ tm('models.configured') }}</h3>
|
||||
<small style="color: grey;" v-if="availableCount">{{ tm('models.available') }} {{ availableCount }}</small>
|
||||
<h3 class="text-h5 font-weight-bold mb-0">
|
||||
{{ tm('models.configured') }}
|
||||
</h3>
|
||||
<small
|
||||
v-if="availableCount"
|
||||
style="color: grey;"
|
||||
>{{ tm('models.available') }} {{ availableCount }}</small>
|
||||
<v-text-field
|
||||
v-model="modelSearchProxy"
|
||||
density="compact"
|
||||
@@ -15,14 +20,14 @@
|
||||
style="max-width: 240px;"
|
||||
:placeholder="tm('models.searchPlaceholder')"
|
||||
/>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-download"
|
||||
:loading="loadingModels"
|
||||
@click="emit('fetch-models')"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
@click="emit('fetch-models')"
|
||||
>
|
||||
{{ isSourceModified ? tm('providerSources.saveAndFetchModels') : tm('providerSources.fetchModels') }}
|
||||
</v-btn>
|
||||
@@ -44,8 +49,15 @@
|
||||
style="max-height: 520px; overflow-y: auto; font-family:system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;"
|
||||
>
|
||||
<template v-if="entries.length > 0">
|
||||
<template v-for="entry in entries" :key="entry.type === 'configured' ? `provider-${entry.provider.id}` : `model-${entry.model}`">
|
||||
<v-tooltip location="top" max-width="400" v-if="entry.type === 'configured'">
|
||||
<template
|
||||
v-for="entry in entries"
|
||||
:key="entry.type === 'configured' ? `provider-${entry.provider.id}` : `model-${entry.model}`"
|
||||
>
|
||||
<v-tooltip
|
||||
v-if="entry.type === 'configured'"
|
||||
location="top"
|
||||
max-width="400"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<v-list-item
|
||||
v-bind="props"
|
||||
@@ -55,63 +67,93 @@
|
||||
<v-list-item-title class="font-weight-medium text-truncate">
|
||||
{{ entry.provider.id }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-caption text-grey d-flex align-center ga-1" style="font-family: monospace;">
|
||||
<span>{{ entry.provider.model }}</span>
|
||||
<v-icon v-if="supportsImageInput(entry.metadata)" size="14" color="grey">
|
||||
mdi-eye-outline
|
||||
</v-icon>
|
||||
<v-icon v-if="supportsToolCall(entry.metadata)" size="14" color="grey">
|
||||
mdi-wrench
|
||||
</v-icon>
|
||||
<v-icon v-if="supportsReasoning(entry.metadata)" size="14" color="grey">
|
||||
mdi-brain
|
||||
</v-icon>
|
||||
<span v-if="formatContextLimit(entry.metadata)">
|
||||
{{ formatContextLimit(entry.metadata) }}
|
||||
</span>
|
||||
</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<div class="d-flex align-center ga-1" @click.stop>
|
||||
<v-switch
|
||||
v-model="entry.provider.enable"
|
||||
density="compact"
|
||||
inset
|
||||
hide-details
|
||||
color="primary"
|
||||
class="mr-1"
|
||||
@update:modelValue="emit('toggle-provider-enable', entry.provider, $event)"
|
||||
></v-switch>
|
||||
<v-tooltip location="top" max-width="300">
|
||||
{{ tm('availability.test') }}
|
||||
<template #activator="{ props }">
|
||||
<v-list-item-subtitle
|
||||
class="text-caption text-grey d-flex align-center ga-1"
|
||||
style="font-family: monospace;"
|
||||
>
|
||||
<span>{{ entry.provider.model }}</span>
|
||||
<v-icon
|
||||
v-if="supportsImageInput(entry.metadata)"
|
||||
size="14"
|
||||
color="grey"
|
||||
>
|
||||
mdi-eye-outline
|
||||
</v-icon>
|
||||
<v-icon
|
||||
v-if="supportsToolCall(entry.metadata)"
|
||||
size="14"
|
||||
color="grey"
|
||||
>
|
||||
mdi-wrench
|
||||
</v-icon>
|
||||
<v-icon
|
||||
v-if="supportsReasoning(entry.metadata)"
|
||||
size="14"
|
||||
color="grey"
|
||||
>
|
||||
mdi-brain
|
||||
</v-icon>
|
||||
<span v-if="formatContextLimit(entry.metadata)">
|
||||
{{ formatContextLimit(entry.metadata) }}
|
||||
</span>
|
||||
</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<div
|
||||
class="d-flex align-center ga-1"
|
||||
@click.stop
|
||||
>
|
||||
<v-switch
|
||||
v-model="entry.provider.enable"
|
||||
density="compact"
|
||||
inset
|
||||
hide-details
|
||||
color="primary"
|
||||
class="mr-1"
|
||||
@update:modelValue="emit('toggle-provider-enable', entry.provider, $event)"
|
||||
/>
|
||||
<v-tooltip
|
||||
location="top"
|
||||
max-width="300"
|
||||
>
|
||||
{{ tm('availability.test') }}
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
icon="mdi-connection"
|
||||
size="small"
|
||||
variant="text"
|
||||
:disabled="!entry.provider.enable"
|
||||
:loading="isProviderTesting(entry.provider.id)"
|
||||
v-bind="props"
|
||||
@click.stop="emit('test-provider', entry.provider)"
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip
|
||||
location="top"
|
||||
max-width="300"
|
||||
>
|
||||
{{ tm('models.configure') }}
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
icon="mdi-cog"
|
||||
size="small"
|
||||
variant="text"
|
||||
v-bind="props"
|
||||
@click.stop="emit('open-provider-edit', entry.provider)"
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-btn
|
||||
icon="mdi-connection"
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
:disabled="!entry.provider.enable"
|
||||
:loading="isProviderTesting(entry.provider.id)"
|
||||
v-bind="props"
|
||||
@click.stop="emit('test-provider', entry.provider)"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip location="top" max-width="300">
|
||||
{{ tm('models.configure') }}
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
icon="mdi-cog"
|
||||
size="small"
|
||||
variant="text"
|
||||
v-bind="props"
|
||||
@click.stop="emit('open-provider-edit', entry.provider)"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-btn icon="mdi-delete" size="small" variant="text" color="error" @click.stop="emit('delete-provider', entry.provider)"></v-btn>
|
||||
</div>
|
||||
</template>
|
||||
color="error"
|
||||
@click.stop="emit('delete-provider', entry.provider)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<div>
|
||||
@@ -120,27 +162,52 @@
|
||||
</div>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip location="top" max-width="400" v-else>
|
||||
<v-tooltip
|
||||
v-else
|
||||
location="top"
|
||||
max-width="400"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<v-list-item v-bind="props" class="cursor-pointer" @click="emit('add-model-provider', entry.model)">
|
||||
<v-list-item
|
||||
v-bind="props"
|
||||
class="cursor-pointer"
|
||||
@click="emit('add-model-provider', entry.model)"
|
||||
>
|
||||
<v-list-item-title>{{ entry.model }}</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-caption text-grey d-flex align-center ga-1">
|
||||
<span>{{ entry.model }}</span>
|
||||
<v-icon v-if="supportsImageInput(entry.metadata)" size="14" color="grey">
|
||||
mdi-eye-outline
|
||||
</v-icon>
|
||||
<v-icon v-if="supportsToolCall(entry.metadata)" size="14" color="grey">
|
||||
mdi-wrench
|
||||
</v-icon>
|
||||
<v-icon v-if="supportsReasoning(entry.metadata)" size="14" color="grey">
|
||||
mdi-brain
|
||||
</v-icon>
|
||||
<span v-if="formatContextLimit(entry.metadata)">
|
||||
{{ formatContextLimit(entry.metadata) }}
|
||||
</span>
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-subtitle class="text-caption text-grey d-flex align-center ga-1">
|
||||
<span>{{ entry.model }}</span>
|
||||
<v-icon
|
||||
v-if="supportsImageInput(entry.metadata)"
|
||||
size="14"
|
||||
color="grey"
|
||||
>
|
||||
mdi-eye-outline
|
||||
</v-icon>
|
||||
<v-icon
|
||||
v-if="supportsToolCall(entry.metadata)"
|
||||
size="14"
|
||||
color="grey"
|
||||
>
|
||||
mdi-wrench
|
||||
</v-icon>
|
||||
<v-icon
|
||||
v-if="supportsReasoning(entry.metadata)"
|
||||
size="14"
|
||||
color="grey"
|
||||
>
|
||||
mdi-brain
|
||||
</v-icon>
|
||||
<span v-if="formatContextLimit(entry.metadata)">
|
||||
{{ formatContextLimit(entry.metadata) }}
|
||||
</span>
|
||||
</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-btn icon="mdi-plus" size="small" variant="text" color="primary"></v-btn>
|
||||
<v-btn
|
||||
icon="mdi-plus"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="primary"
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
@@ -152,8 +219,15 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="text-center pa-4 text-medium-emphasis">
|
||||
<v-icon size="48" color="grey-lighten-1">mdi-package-variant</v-icon>
|
||||
<p class="text-grey mt-2">{{ tm('models.empty') }}</p>
|
||||
<v-icon
|
||||
size="48"
|
||||
color="grey-lighten-1"
|
||||
>
|
||||
mdi-package-variant
|
||||
</v-icon>
|
||||
<p class="text-grey mt-2">
|
||||
{{ tm('models.empty') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</v-list>
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
<template>
|
||||
<v-card class="provider-sources-panel h-100" elevation="0">
|
||||
<v-card
|
||||
class="provider-sources-panel h-100"
|
||||
elevation="0"
|
||||
>
|
||||
<div class="d-flex align-center justify-space-between px-4 pt-4 pb-2">
|
||||
<div class="d-flex align-center ga-2">
|
||||
<h3 class="mb-0">{{ tm('providerSources.title') }}</h3>
|
||||
<h3 class="mb-0">
|
||||
{{ tm('providerSources.title') }}
|
||||
</h3>
|
||||
</div>
|
||||
<StyledMenu>
|
||||
<template #activator="{ props }">
|
||||
@@ -24,9 +29,23 @@
|
||||
@click="emitAddSource(sourceType.value)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-avatar size="18" rounded="0" class="me-2">
|
||||
<v-img v-if="sourceType.icon" :src="sourceType.icon" alt="provider icon" cover></v-img>
|
||||
<v-icon v-else size="16">mdi-shape-outline</v-icon>
|
||||
<v-avatar
|
||||
size="18"
|
||||
rounded="0"
|
||||
class="me-2"
|
||||
>
|
||||
<v-img
|
||||
v-if="sourceType.icon"
|
||||
:src="sourceType.icon"
|
||||
alt="provider icon"
|
||||
cover
|
||||
/>
|
||||
<v-icon
|
||||
v-else
|
||||
size="16"
|
||||
>
|
||||
mdi-shape-outline
|
||||
</v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<v-list-item-title>{{ sourceType.label }}</v-list-item-title>
|
||||
@@ -34,7 +53,10 @@
|
||||
</StyledMenu>
|
||||
</div>
|
||||
|
||||
<div v-if="isMobile && displayedProviderSources.length > 0" class="px-4 pb-3">
|
||||
<div
|
||||
v-if="isMobile && displayedProviderSources.length > 0"
|
||||
class="px-4 pb-3"
|
||||
>
|
||||
<div class="d-flex align-center ga-2">
|
||||
<v-select
|
||||
:model-value="selectedId"
|
||||
@@ -52,9 +74,23 @@
|
||||
<template #item="{ props: itemProps, item }">
|
||||
<v-list-item v-bind="itemProps">
|
||||
<template #prepend>
|
||||
<v-avatar size="18" rounded="0" class="me-2">
|
||||
<v-img v-if="item.raw.icon" :src="item.raw.icon" alt="provider icon" cover></v-img>
|
||||
<v-icon v-else size="16">mdi-shape-outline</v-icon>
|
||||
<v-avatar
|
||||
size="18"
|
||||
rounded="0"
|
||||
class="me-2"
|
||||
>
|
||||
<v-img
|
||||
v-if="item.raw.icon"
|
||||
:src="item.raw.icon"
|
||||
alt="provider icon"
|
||||
cover
|
||||
/>
|
||||
<v-icon
|
||||
v-else
|
||||
size="16"
|
||||
>
|
||||
mdi-shape-outline
|
||||
</v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
</v-list-item>
|
||||
@@ -67,12 +103,17 @@
|
||||
size="small"
|
||||
color="error"
|
||||
@click.stop="emitDeleteSource(selectedProviderSource)"
|
||||
></v-btn>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="displayedProviderSources.length > 0">
|
||||
<v-list class="provider-source-list" nav density="compact" lines="two">
|
||||
<v-list
|
||||
class="provider-source-list"
|
||||
nav
|
||||
density="compact"
|
||||
lines="two"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="source in displayedProviderSources"
|
||||
:key="source.isPlaceholder ? `template-${source.templateKey}` : source.id"
|
||||
@@ -83,13 +124,34 @@
|
||||
@click="emitSelectSource(source)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-avatar size="32" class="bg-grey-lighten-4" rounded="0">
|
||||
<v-img v-if="source?.provider" :src="resolveSourceIcon(source)" alt="logo" cover></v-img>
|
||||
<v-icon v-else size="32">mdi-creation</v-icon>
|
||||
<v-avatar
|
||||
size="32"
|
||||
class="bg-grey-lighten-4"
|
||||
rounded="0"
|
||||
>
|
||||
<v-img
|
||||
v-if="source?.provider"
|
||||
:src="resolveSourceIcon(source)"
|
||||
alt="logo"
|
||||
cover
|
||||
/>
|
||||
<v-icon
|
||||
v-else
|
||||
size="32"
|
||||
>
|
||||
mdi-creation
|
||||
</v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<v-list-item-title class="font-weight-bold mb-1" style="font-family: Arial, Helvetica, sans-serif; font-size: 16px;">{{ getSourceDisplayName(source) }}</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-truncate">{{ source.api_base || 'N/A' }}</v-list-item-subtitle>
|
||||
<v-list-item-title
|
||||
class="font-weight-bold mb-1"
|
||||
style="font-family: Arial, Helvetica, sans-serif; font-size: 16px;"
|
||||
>
|
||||
{{ getSourceDisplayName(source) }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-truncate">
|
||||
{{ source.api_base || 'N/A' }}
|
||||
</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<div class="d-flex align-center ga-1">
|
||||
<v-btn
|
||||
@@ -99,15 +161,25 @@
|
||||
size="x-small"
|
||||
color="error"
|
||||
@click.stop="emitDeleteSource(source)"
|
||||
></v-btn>
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
<div v-else class="text-center py-8 px-4">
|
||||
<v-icon size="48" color="grey-lighten-1">mdi-api-off</v-icon>
|
||||
<p class="text-grey mt-2">{{ tm('providerSources.empty') }}</p>
|
||||
<div
|
||||
v-else
|
||||
class="text-center py-8 px-4"
|
||||
>
|
||||
<v-icon
|
||||
size="48"
|
||||
color="grey-lighten-1"
|
||||
>
|
||||
mdi-api-off
|
||||
</v-icon>
|
||||
<p class="text-grey mt-2">
|
||||
{{ tm('providerSources.empty') }}
|
||||
</p>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
@@ -155,6 +155,10 @@ function getItemPath(key) {
|
||||
return props.pathPrefix ? `${props.pathPrefix}.${key}` : key
|
||||
}
|
||||
|
||||
function setIterableValue(key, value) {
|
||||
Reflect.set(props.iterable, key, value)
|
||||
}
|
||||
|
||||
function hasVisibleItemsAfter(items, currentIndex) {
|
||||
const itemEntries = Object.entries(items)
|
||||
|
||||
@@ -172,19 +176,28 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="config-section" v-if="iterable && metadata[metadataKey]?.type === 'object'">
|
||||
<div
|
||||
v-if="iterable && metadata[metadataKey]?.type === 'object'"
|
||||
class="config-section"
|
||||
>
|
||||
<v-list-item-title class="config-title">
|
||||
{{ translateIfKey(metadata[metadataKey]?.description) }} <span class="metadata-key">({{ metadataKey }})</span>
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="config-hint">
|
||||
<span v-if="metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint" class="important-hint">‼️</span>
|
||||
<span
|
||||
v-if="metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint"
|
||||
class="important-hint"
|
||||
>‼️</span>
|
||||
{{ translateIfKey(metadata[metadataKey]?.hint) }}
|
||||
</v-list-item-subtitle>
|
||||
</div>
|
||||
|
||||
<v-card-text class="px-0 py-1">
|
||||
<!-- Object Type Configuration -->
|
||||
<div v-if="metadata[metadataKey]?.type === 'object' || metadata[metadataKey]?.config_template" class="object-config">
|
||||
<div
|
||||
v-if="metadata[metadataKey]?.type === 'object' || metadata[metadataKey]?.config_template"
|
||||
class="object-config"
|
||||
>
|
||||
<!-- Provider-level hint -->
|
||||
<v-alert
|
||||
v-if="providerHint"
|
||||
@@ -197,26 +210,41 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
{{ translateIfKey(providerHint) }}
|
||||
</v-alert>
|
||||
|
||||
<div v-for="(val, key, index) in filteredIterable" :key="key" class="config-item">
|
||||
<div
|
||||
v-for="(val, key, index) in filteredIterable"
|
||||
:key="key"
|
||||
class="config-item"
|
||||
>
|
||||
<!-- Nested Object -->
|
||||
<div v-if="metadata[metadataKey].items[key]?.type === 'object'" class="nested-object">
|
||||
<div v-if="metadata[metadataKey].items[key] && !metadata[metadataKey].items[key]?.invisible && shouldShowItem(metadata[metadataKey].items[key], key)" class="nested-container">
|
||||
<div
|
||||
v-if="metadata[metadataKey].items[key]?.type === 'object'"
|
||||
class="nested-object"
|
||||
>
|
||||
<div
|
||||
v-if="metadata[metadataKey].items[key] && !metadata[metadataKey].items[key]?.invisible && shouldShowItem(metadata[metadataKey].items[key], key)"
|
||||
class="nested-container"
|
||||
>
|
||||
<v-expand-transition>
|
||||
<AstrBotConfig
|
||||
:metadata="metadata[metadataKey].items"
|
||||
:iterable="iterable[key]"
|
||||
:metadataKey="key"
|
||||
:pluginName="pluginName"
|
||||
:pathPrefix="getItemPath(key)"
|
||||
>
|
||||
</AstrBotConfig>
|
||||
:metadata-key="key"
|
||||
:plugin-name="pluginName"
|
||||
:path-prefix="getItemPath(key)"
|
||||
/>
|
||||
</v-expand-transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Template List -->
|
||||
<div v-else-if="metadata[metadataKey].items[key]?.type === 'template_list'" class="nested-object w-100">
|
||||
<div v-if="!metadata[metadataKey].items[key]?.invisible && shouldShowItem(metadata[metadataKey].items[key], key)" class="nested-container">
|
||||
<div
|
||||
v-else-if="metadata[metadataKey].items[key]?.type === 'template_list'"
|
||||
class="nested-object w-100"
|
||||
>
|
||||
<div
|
||||
v-if="!metadata[metadataKey].items[key]?.invisible && shouldShowItem(metadata[metadataKey].items[key], key)"
|
||||
class="nested-container"
|
||||
>
|
||||
<div class="config-section mb-2">
|
||||
<v-list-item-title class="config-title">
|
||||
<span v-if="metadata[metadataKey].items[key]?.description">
|
||||
@@ -226,22 +254,34 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
<span v-else>{{ key }}</span>
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="config-hint">
|
||||
<span v-if="metadata[metadataKey].items[key]?.obvious_hint && metadata[metadataKey].items[key]?.hint" class="important-hint">‼️</span>
|
||||
<span
|
||||
v-if="metadata[metadataKey].items[key]?.obvious_hint && metadata[metadataKey].items[key]?.hint"
|
||||
class="important-hint"
|
||||
>‼️</span>
|
||||
{{ translateIfKey(metadata[metadataKey].items[key]?.hint) }}
|
||||
</v-list-item-subtitle>
|
||||
</div>
|
||||
<!-- eslint-disable-next-line vue/no-mutating-props -->
|
||||
<TemplateListEditor
|
||||
v-model="iterable[key]"
|
||||
:model-value="iterable[key]"
|
||||
:templates="metadata[metadataKey].items[key]?.templates || {}"
|
||||
class="config-field"
|
||||
@update:model-value="setIterableValue(key, $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Regular Property -->
|
||||
<template v-else>
|
||||
<v-row v-if="!metadata[metadataKey].items[key]?.invisible && shouldShowItem(metadata[metadataKey].items[key], key)" class="config-row">
|
||||
<v-col cols="12" sm="6" class="property-info">
|
||||
<v-row
|
||||
v-if="!metadata[metadataKey].items[key]?.invisible && shouldShowItem(metadata[metadataKey].items[key], key)"
|
||||
class="config-row"
|
||||
>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
class="property-info"
|
||||
>
|
||||
<v-list-item density="compact">
|
||||
<v-list-item-title class="property-name">
|
||||
<span v-if="metadata[metadataKey].items[key]?.description">
|
||||
@@ -252,21 +292,29 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle class="property-hint">
|
||||
<span v-if="metadata[metadataKey].items[key]?.obvious_hint && getItemHint(key, metadata[metadataKey].items[key])"
|
||||
class="important-hint">‼️</span>
|
||||
<span
|
||||
v-if="metadata[metadataKey].items[key]?.obvious_hint && getItemHint(key, metadata[metadataKey].items[key])"
|
||||
class="important-hint"
|
||||
>‼️</span>
|
||||
{{ translateIfKey(getItemHint(key, metadata[metadataKey].items[key])) }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" class="config-input">
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
class="config-input"
|
||||
>
|
||||
<!-- eslint-disable-next-line vue/no-mutating-props -->
|
||||
<ConfigItemRenderer
|
||||
v-model="iterable[key]"
|
||||
:model-value="iterable[key]"
|
||||
:item-meta="metadata[metadataKey].items[key] || null"
|
||||
:plugin-name="pluginName"
|
||||
:config-key="getItemPath(key)"
|
||||
:loading="loadingEmbeddingDim"
|
||||
:show-fullscreen-btn="!!metadata[metadataKey].items[key]?.editor_mode"
|
||||
@update:model-value="setIterableValue(key, $event)"
|
||||
@get-embedding-dim="getEmbeddingDimensions(iterable)"
|
||||
@open-fullscreen="openEditorDialog(key, iterable, metadata[metadataKey].items[key]?.editor_theme, metadata[metadataKey].items[key]?.editor_language)"
|
||||
/>
|
||||
@@ -276,15 +324,22 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
<v-divider
|
||||
v-if="hasVisibleItemsAfter(filteredIterable, index) && !metadata[metadataKey].items[key]?.invisible && shouldShowItem(metadata[metadataKey].items[key], key)"
|
||||
class="config-divider"
|
||||
></v-divider>
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Simple Value Configuration -->
|
||||
<div v-else class="simple-config">
|
||||
<div
|
||||
v-else
|
||||
class="simple-config"
|
||||
>
|
||||
<v-row class="config-row">
|
||||
<v-col cols="12" sm="7" class="property-info">
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="7"
|
||||
class="property-info"
|
||||
>
|
||||
<v-list-item density="compact">
|
||||
<v-list-item-title class="property-name">
|
||||
{{ metadata[metadataKey]?.description }}
|
||||
@@ -292,54 +347,80 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle class="property-hint">
|
||||
<span v-if="metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint" class="important-hint">‼️</span>
|
||||
<span
|
||||
v-if="metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint"
|
||||
class="important-hint"
|
||||
>‼️</span>
|
||||
{{ metadata[metadataKey]?.hint }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="5" class="config-input">
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="5"
|
||||
class="config-input"
|
||||
>
|
||||
<!-- eslint-disable-next-line vue/no-mutating-props -->
|
||||
<TemplateListEditor
|
||||
v-if="metadata[metadataKey]?.type === 'template_list' && !metadata[metadataKey]?.invisible"
|
||||
v-model="iterable[metadataKey]"
|
||||
:model-value="iterable[metadataKey]"
|
||||
:templates="metadata[metadataKey]?.templates || {}"
|
||||
class="config-field"
|
||||
@update:model-value="setIterableValue(metadataKey, $event)"
|
||||
/>
|
||||
<!-- eslint-disable-next-line vue/no-mutating-props -->
|
||||
<ConfigItemRenderer
|
||||
v-else
|
||||
v-model="iterable[metadataKey]"
|
||||
:model-value="iterable[metadataKey]"
|
||||
:item-meta="metadata[metadataKey]"
|
||||
:plugin-name="pluginName"
|
||||
:config-key="getItemPath(metadataKey)"
|
||||
@update:model-value="setIterableValue(metadataKey, $event)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider class="my-2 config-divider"></v-divider>
|
||||
<v-divider class="my-2 config-divider" />
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Full Screen Editor Dialog -->
|
||||
<v-dialog v-model="dialog" fullscreen transition="dialog-bottom-transition" scrollable>
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
fullscreen
|
||||
transition="dialog-bottom-transition"
|
||||
scrollable
|
||||
>
|
||||
<v-card>
|
||||
<v-toolbar color="primary" dark>
|
||||
<v-btn icon @click="dialog = false">
|
||||
<v-toolbar
|
||||
color="primary"
|
||||
dark
|
||||
>
|
||||
<v-btn
|
||||
icon
|
||||
@click="dialog = false"
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
<v-toolbar-title>{{ t('core.common.editor.editingTitle') }} - {{ currentEditingKey }}</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-toolbar-items>
|
||||
<v-btn variant="text" @click="saveEditedContent">{{ t('core.common.save') }}</v-btn>
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="saveEditedContent"
|
||||
>
|
||||
{{ t('core.common.save') }}
|
||||
</v-btn>
|
||||
</v-toolbar-items>
|
||||
</v-toolbar>
|
||||
<v-card-text class="pa-0">
|
||||
<VueMonacoEditor
|
||||
v-model:value="currentEditingKeyIterable[currentEditingKey]"
|
||||
:theme="currentEditingTheme"
|
||||
:language="currentEditingLanguage"
|
||||
style="height: calc(100vh - 64px);"
|
||||
v-model:value="currentEditingKeyIterable[currentEditingKey]"
|
||||
>
|
||||
</VueMonacoEditor>
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
@@ -206,28 +206,51 @@ function getSpecialSubtype(value) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
|
||||
<v-card v-if="shouldShowSection()" style="margin-bottom: 16px; padding-bottom: 8px; background-color: rgb(var(--v-theme-background));"
|
||||
rounded="md" variant="outlined">
|
||||
<v-card-text class="config-section" v-if="metadata[metadataKey]?.type === 'object'" style="padding-bottom: 8px;">
|
||||
<v-card
|
||||
v-if="shouldShowSection()"
|
||||
style="margin-bottom: 16px; padding-bottom: 8px; background-color: rgb(var(--v-theme-background));"
|
||||
rounded="md"
|
||||
variant="outlined"
|
||||
>
|
||||
<v-card-text
|
||||
v-if="metadata[metadataKey]?.type === 'object'"
|
||||
class="config-section"
|
||||
style="padding-bottom: 8px;"
|
||||
>
|
||||
<v-list-item-title class="config-title">
|
||||
{{ translateIfKey(metadata[metadataKey]?.description) }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="config-hint">
|
||||
<span v-if="metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint" class="important-hint">‼️</span>
|
||||
<span v-html="renderHint(metadata[metadataKey]?.hint)"></span>
|
||||
<span
|
||||
v-if="metadata[metadataKey]?.obvious_hint && metadata[metadataKey]?.hint"
|
||||
class="important-hint"
|
||||
>‼️</span>
|
||||
<span v-html="renderHint(metadata[metadataKey]?.hint)" />
|
||||
</v-list-item-subtitle>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Object Type Configuration with JSON Selector Support -->
|
||||
<div v-if="metadata[metadataKey]?.type === 'object'" class="object-config">
|
||||
<div v-for="(itemMeta, itemKey, index) in metadata[metadataKey].items" :key="itemKey" class="config-item">
|
||||
<div
|
||||
v-if="metadata[metadataKey]?.type === 'object'"
|
||||
class="object-config"
|
||||
>
|
||||
<div
|
||||
v-for="(itemMeta, itemKey, index) in metadata[metadataKey].items"
|
||||
:key="itemKey"
|
||||
class="config-item"
|
||||
>
|
||||
<!-- Check if itemKey is a JSON selector -->
|
||||
<template v-if="shouldShowItem(itemMeta, itemKey)">
|
||||
<!-- JSON Selector Property -->
|
||||
<v-row v-if="!itemMeta?.invisible" class="config-row">
|
||||
<v-col cols="12" sm="6" class="property-info">
|
||||
<v-row
|
||||
v-if="!itemMeta?.invisible"
|
||||
class="config-row"
|
||||
>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
class="property-info"
|
||||
>
|
||||
<v-list-item density="compact">
|
||||
<v-list-item-title class="property-name">
|
||||
{{ translateIfKey(itemMeta?.description) || itemKey }}
|
||||
@@ -235,12 +258,19 @@ function getSpecialSubtype(value) {
|
||||
</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle class="property-hint">
|
||||
<span v-if="itemMeta?.obvious_hint && itemMeta?.hint" class="important-hint">‼️</span>
|
||||
<span v-html="renderHint(itemMeta?.hint)"></span>
|
||||
<span
|
||||
v-if="itemMeta?.obvious_hint && itemMeta?.hint"
|
||||
class="important-hint"
|
||||
>‼️</span>
|
||||
<span v-html="renderHint(itemMeta?.hint)" />
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" class="config-input">
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
class="config-input"
|
||||
>
|
||||
<TemplateListEditor
|
||||
v-if="itemMeta?.type === 'template_list'"
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
@@ -258,17 +288,30 @@ function getSpecialSubtype(value) {
|
||||
</v-row>
|
||||
|
||||
<!-- Plugin Set Selector 全宽显示区域 -->
|
||||
<v-row v-if="!itemMeta?.invisible && itemMeta?._special === 'select_plugin_set'"
|
||||
class="plugin-set-display-row">
|
||||
<v-col cols="12" class="plugin-set-display">
|
||||
<div v-if="createSelectorModel(itemKey).value && createSelectorModel(itemKey).value.length > 0"
|
||||
class="selected-plugins-full-width">
|
||||
<v-row
|
||||
v-if="!itemMeta?.invisible && itemMeta?._special === 'select_plugin_set'"
|
||||
class="plugin-set-display-row"
|
||||
>
|
||||
<v-col
|
||||
cols="12"
|
||||
class="plugin-set-display"
|
||||
>
|
||||
<div
|
||||
v-if="createSelectorModel(itemKey).value && createSelectorModel(itemKey).value.length > 0"
|
||||
class="selected-plugins-full-width"
|
||||
>
|
||||
<div class="plugins-header">
|
||||
<small class="text-grey">{{ t('core.shared.pluginSetSelector.selectedPluginsLabel') }}</small>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap ga-2 mt-2">
|
||||
<v-chip v-for="plugin in (createSelectorModel(itemKey).value || [])" :key="plugin" size="small" label
|
||||
color="primary" variant="outlined">
|
||||
<v-chip
|
||||
v-for="plugin in (createSelectorModel(itemKey).value || [])"
|
||||
:key="plugin"
|
||||
size="small"
|
||||
label
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
>
|
||||
{{ plugin === '*' ? t('core.shared.pluginSetSelector.allPluginsLabel') : plugin }}
|
||||
</v-chip>
|
||||
</div>
|
||||
@@ -281,35 +324,58 @@ function getSpecialSubtype(value) {
|
||||
v-if="!itemMeta?.invisible && itemMeta?._special === 'select_persona' && itemKey === 'provider_settings.default_personality'"
|
||||
class="persona-preview-row"
|
||||
>
|
||||
<v-col cols="12" class="persona-preview-display">
|
||||
<v-col
|
||||
cols="12"
|
||||
class="persona-preview-display"
|
||||
>
|
||||
<PersonaQuickPreview :model-value="createSelectorModel(itemKey).value" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<v-divider class="config-divider"
|
||||
v-if="shouldShowItem(itemMeta, itemKey) && hasVisibleItemsAfter(metadata[metadataKey].items, index)"></v-divider>
|
||||
<v-divider
|
||||
v-if="shouldShowItem(itemMeta, itemKey) && hasVisibleItemsAfter(metadata[metadataKey].items, index)"
|
||||
class="config-divider"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
<!-- Full Screen Editor Dialog -->
|
||||
<v-dialog v-model="dialog" fullscreen transition="dialog-bottom-transition" scrollable>
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
fullscreen
|
||||
transition="dialog-bottom-transition"
|
||||
scrollable
|
||||
>
|
||||
<v-card>
|
||||
<v-toolbar color="primary" dark>
|
||||
<v-btn icon @click="dialog = false">
|
||||
<v-toolbar
|
||||
color="primary"
|
||||
dark
|
||||
>
|
||||
<v-btn
|
||||
icon
|
||||
@click="dialog = false"
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
<v-toolbar-title>{{ t('core.common.editor.editingTitle') }} - {{ currentEditingKey }}</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-toolbar-items>
|
||||
<v-btn variant="text" @click="saveEditedContent">{{ t('core.common.save') }}</v-btn>
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="saveEditedContent"
|
||||
>
|
||||
{{ t('core.common.save') }}
|
||||
</v-btn>
|
||||
</v-toolbar-items>
|
||||
</v-toolbar>
|
||||
<v-card-text class="pa-0">
|
||||
<VueMonacoEditor :theme="currentEditingTheme" :language="currentEditingLanguage"
|
||||
style="height: calc(100vh - 64px);" v-model:value="currentEditingKeyIterable[currentEditingKey]">
|
||||
</VueMonacoEditor>
|
||||
<VueMonacoEditor
|
||||
v-model:value="currentEditingKeyIterable[currentEditingKey]"
|
||||
:theme="currentEditingTheme"
|
||||
:language="currentEditingLanguage"
|
||||
style="height: calc(100vh - 64px);"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -139,15 +139,19 @@ getCurrentVersion();
|
||||
<template>
|
||||
<v-dialog
|
||||
:model-value="dialog"
|
||||
@update:model-value="dialog = $event"
|
||||
:width="$vuetify.display.smAndDown ? '100%' : '800'"
|
||||
:fullscreen="$vuetify.display.xs"
|
||||
max-width="1000"
|
||||
:fullscreen="$vuetify.display.xs"
|
||||
max-width="1000"
|
||||
@update:model-value="dialog = $event"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<span class="text-h3">{{ t('core.navigation.changelogDialog.title') }}</span>
|
||||
<v-btn icon @click="dialog = false" flat>
|
||||
<v-btn
|
||||
icon
|
||||
flat
|
||||
@click="dialog = false"
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
@@ -163,16 +167,26 @@ getCurrentVersion();
|
||||
density="compact"
|
||||
@update:model-value="onVersionChange"
|
||||
>
|
||||
<template v-slot:item="{ item, props }">
|
||||
<v-list-item v-bind="props" :title="`v${item.value}`">
|
||||
<template v-slot:append v-if="item.value === changelogVersion">
|
||||
<v-chip size="x-small" color="primary" variant="tonal">
|
||||
<template #item="{ item, props }">
|
||||
<v-list-item
|
||||
v-bind="props"
|
||||
:title="`v${item.value}`"
|
||||
>
|
||||
<template
|
||||
v-if="item.value === changelogVersion"
|
||||
#append
|
||||
>
|
||||
<v-chip
|
||||
size="x-small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ t('core.navigation.changelogDialog.current') }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<template v-slot:selection="{ item }">
|
||||
<template #selection="{ item }">
|
||||
<span>v{{ item.value }}</span>
|
||||
</template>
|
||||
</v-select>
|
||||
@@ -180,21 +194,45 @@ getCurrentVersion();
|
||||
|
||||
<!-- 更新日志内容 -->
|
||||
<div style="max-height: 70vh; overflow-y: auto;">
|
||||
<div v-if="changelogLoading" class="text-center py-8">
|
||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||
<div class="mt-4">{{ t('core.navigation.changelogDialog.loading') }}</div>
|
||||
<div
|
||||
v-if="changelogLoading"
|
||||
class="text-center py-8"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
<div class="mt-4">
|
||||
{{ t('core.navigation.changelogDialog.loading') }}
|
||||
</div>
|
||||
</div>
|
||||
<v-alert v-else-if="changelogError" type="error" variant="tonal" border="start">
|
||||
<v-alert
|
||||
v-else-if="changelogError"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
border="start"
|
||||
>
|
||||
{{ changelogError }}
|
||||
</v-alert>
|
||||
<div v-else-if="changelogContent" class="changelog-content">
|
||||
<MarkdownRender :content="changelogContent" :typewriter="false" class="markdown-content" />
|
||||
<div
|
||||
v-else-if="changelogContent"
|
||||
class="changelog-content"
|
||||
>
|
||||
<MarkdownRender
|
||||
:content="changelogContent"
|
||||
:typewriter="false"
|
||||
class="markdown-content"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="blue-darken-1" variant="text" @click="dialog = false">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue-darken-1"
|
||||
variant="text"
|
||||
@click="dialog = false"
|
||||
>
|
||||
{{ t('core.common.close') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
|
||||
@@ -2,45 +2,74 @@
|
||||
<div class="w-100">
|
||||
<!-- Special handling for specific metadata types -->
|
||||
<template v-if="itemMeta?._special === 'select_provider'">
|
||||
<ProviderSelector :model-value="modelValue" @update:model-value="emitUpdate" :provider-type="'chat_completion'" />
|
||||
<ProviderSelector
|
||||
:model-value="modelValue"
|
||||
:provider-type="'chat_completion'"
|
||||
@update:model-value="emitUpdate"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="itemMeta?._special === 'select_provider_stt'">
|
||||
<ProviderSelector :model-value="modelValue" @update:model-value="emitUpdate" :provider-type="'speech_to_text'" />
|
||||
<ProviderSelector
|
||||
:model-value="modelValue"
|
||||
:provider-type="'speech_to_text'"
|
||||
@update:model-value="emitUpdate"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="itemMeta?._special === 'select_provider_tts'">
|
||||
<ProviderSelector :model-value="modelValue" @update:model-value="emitUpdate" :provider-type="'text_to_speech'" />
|
||||
<ProviderSelector
|
||||
:model-value="modelValue"
|
||||
:provider-type="'text_to_speech'"
|
||||
@update:model-value="emitUpdate"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="itemMeta?._special === 'select_providers'">
|
||||
<ProviderSelector
|
||||
:model-value="modelValue"
|
||||
@update:model-value="emitUpdate"
|
||||
:provider-type="'chat_completion'"
|
||||
:multiple="true"
|
||||
@update:model-value="emitUpdate"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="getSpecialName(itemMeta?._special) === 'select_agent_runner_provider'">
|
||||
<ProviderSelector
|
||||
:model-value="modelValue"
|
||||
@update:model-value="emitUpdate"
|
||||
:provider-type="'agent_runner'"
|
||||
:provider-subtype="getSpecialSubtype(itemMeta?._special)"
|
||||
@update:model-value="emitUpdate"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="itemMeta?._special === 'provider_pool'">
|
||||
<ProviderSelector :model-value="modelValue" @update:model-value="emitUpdate" :provider-type="'chat_completion'"
|
||||
:button-text="t('core.shared.providerSelector.selectProviderPool')" />
|
||||
<ProviderSelector
|
||||
:model-value="modelValue"
|
||||
:provider-type="'chat_completion'"
|
||||
:button-text="t('core.shared.providerSelector.selectProviderPool')"
|
||||
@update:model-value="emitUpdate"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="itemMeta?._special === 'select_persona'">
|
||||
<PersonaSelector :model-value="modelValue" @update:model-value="emitUpdate" />
|
||||
<PersonaSelector
|
||||
:model-value="modelValue"
|
||||
@update:model-value="emitUpdate"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="itemMeta?._special === 'persona_pool'">
|
||||
<PersonaSelector :model-value="modelValue" @update:model-value="emitUpdate" :button-text="t('core.shared.personaSelector.selectPersonaPool')" />
|
||||
<PersonaSelector
|
||||
:model-value="modelValue"
|
||||
:button-text="t('core.shared.personaSelector.selectPersonaPool')"
|
||||
@update:model-value="emitUpdate"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="itemMeta?._special === 'select_knowledgebase'">
|
||||
<KnowledgeBaseSelector :model-value="modelValue" @update:model-value="emitUpdate" />
|
||||
<KnowledgeBaseSelector
|
||||
:model-value="modelValue"
|
||||
@update:model-value="emitUpdate"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="itemMeta?._special === 'select_plugin_set'">
|
||||
<PluginSetSelector :model-value="modelValue" @update:model-value="emitUpdate" />
|
||||
<PluginSetSelector
|
||||
:model-value="modelValue"
|
||||
@update:model-value="emitUpdate"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="itemMeta?._special === 't2i_template'">
|
||||
<T2ITemplateEditor />
|
||||
@@ -49,20 +78,20 @@
|
||||
<div class="d-flex align-center gap-2">
|
||||
<v-text-field
|
||||
:model-value="modelValue"
|
||||
@update:model-value="emitUpdate"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="config-field"
|
||||
type="number"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
@update:model-value="emitUpdate"
|
||||
/>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
@click="$emit('get-embedding-dim')"
|
||||
:loading="loading"
|
||||
class="ml-2"
|
||||
@click="$emit('get-embedding-dim')"
|
||||
>
|
||||
{{ t('core.common.autoDetect') }}
|
||||
</v-btn>
|
||||
@@ -77,19 +106,18 @@
|
||||
v-for="(option, optionIndex) in itemMeta.options"
|
||||
:key="optionIndex"
|
||||
:model-value="modelValue"
|
||||
@update:model-value="emitUpdate"
|
||||
:label="getLabel(itemMeta, optionIndex, option)"
|
||||
:value="option"
|
||||
class="mr-2"
|
||||
color="primary"
|
||||
hide-details
|
||||
></v-checkbox>
|
||||
@update:model-value="emitUpdate"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<v-combobox
|
||||
v-else-if="itemMeta?.type === 'list' && itemMeta?.options"
|
||||
:model-value="modelValue"
|
||||
@update:model-value="emitUpdate"
|
||||
:items="itemMeta.options"
|
||||
:disabled="itemMeta?.readonly"
|
||||
density="compact"
|
||||
@@ -98,32 +126,42 @@
|
||||
hide-details
|
||||
chips
|
||||
multiple
|
||||
></v-combobox>
|
||||
@update:model-value="emitUpdate"
|
||||
/>
|
||||
|
||||
<v-select
|
||||
v-else-if="itemMeta?.options"
|
||||
:model-value="modelValue"
|
||||
@update:model-value="emitUpdate"
|
||||
:items="getSelectItems(itemMeta)"
|
||||
:disabled="itemMeta?.readonly"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="config-field"
|
||||
hide-details
|
||||
></v-select>
|
||||
@update:model-value="emitUpdate"
|
||||
/>
|
||||
|
||||
<div v-else-if="itemMeta?.editor_mode" class="editor-container">
|
||||
<div
|
||||
v-else-if="itemMeta?.editor_mode"
|
||||
class="editor-container"
|
||||
>
|
||||
<VueMonacoEditor
|
||||
:theme="itemMeta?.editor_theme || 'vs-light'"
|
||||
:language="itemMeta?.editor_language || 'json'"
|
||||
style="min-height: 100px; flex-grow: 1; border: 1px solid rgba(0, 0, 0, 0.1);"
|
||||
:value="modelValue"
|
||||
@update:value="emitUpdate"
|
||||
>
|
||||
</VueMonacoEditor>
|
||||
<v-btn v-if="showFullscreenBtn" icon size="small" variant="text" color="primary" class="editor-fullscreen-btn"
|
||||
/>
|
||||
<v-btn
|
||||
v-if="showFullscreenBtn"
|
||||
icon
|
||||
size="small"
|
||||
variant="text"
|
||||
color="primary"
|
||||
class="editor-fullscreen-btn"
|
||||
:title="t('core.common.editor.fullscreen')"
|
||||
@click="$emit('open-fullscreen')"
|
||||
:title="t('core.common.editor.fullscreen')">
|
||||
>
|
||||
<v-icon>mdi-fullscreen</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
@@ -131,12 +169,12 @@
|
||||
<v-text-field
|
||||
v-else-if="itemMeta?.type === 'string'"
|
||||
:model-value="modelValue"
|
||||
@update:model-value="emitUpdate"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="config-field"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
@update:model-value="emitUpdate"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-else-if="itemMeta?.type === 'int' || itemMeta?.type === 'float'"
|
||||
@@ -145,7 +183,6 @@
|
||||
<v-slider
|
||||
v-if="itemMeta?.slider"
|
||||
:model-value="toNumber(modelValue)"
|
||||
@update:model-value="val => emitUpdate(toNumber(val))"
|
||||
:min="itemMeta?.slider?.min ?? 0"
|
||||
:max="itemMeta?.slider?.max ?? 100"
|
||||
:step="itemMeta?.slider?.step ?? 1"
|
||||
@@ -153,38 +190,39 @@
|
||||
density="compact"
|
||||
hide-details
|
||||
style="flex: 1"
|
||||
></v-slider>
|
||||
@update:model-value="val => emitUpdate(toNumber(val))"
|
||||
/>
|
||||
<v-text-field
|
||||
:model-value="modelValue"
|
||||
@update:model-value="val => emitUpdate(toNumber(val))"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="config-field"
|
||||
type="number"
|
||||
hide-details
|
||||
style="flex: 1"
|
||||
></v-text-field>
|
||||
@update:model-value="val => emitUpdate(toNumber(val))"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<v-textarea
|
||||
v-else-if="itemMeta?.type === 'text'"
|
||||
:model-value="modelValue"
|
||||
@update:model-value="emitUpdate"
|
||||
variant="outlined"
|
||||
rows="3"
|
||||
class="config-field"
|
||||
hide-details
|
||||
></v-textarea>
|
||||
@update:model-value="emitUpdate"
|
||||
/>
|
||||
|
||||
<v-switch
|
||||
v-else-if="itemMeta?.type === 'bool'"
|
||||
:model-value="modelValue"
|
||||
@update:model-value="emitUpdate"
|
||||
color="primary"
|
||||
inset
|
||||
density="compact"
|
||||
hide-details
|
||||
></v-switch>
|
||||
@update:model-value="emitUpdate"
|
||||
/>
|
||||
|
||||
<FileConfigItem
|
||||
v-else-if="itemMeta?.type === 'file'"
|
||||
@@ -192,34 +230,34 @@
|
||||
:item-meta="itemMeta"
|
||||
:plugin-name="pluginName"
|
||||
:config-key="configKey"
|
||||
@update:model-value="emitUpdate"
|
||||
class="config-field"
|
||||
@update:model-value="emitUpdate"
|
||||
/>
|
||||
|
||||
<ListConfigItem
|
||||
v-else-if="itemMeta?.type === 'list'"
|
||||
:model-value="modelValue"
|
||||
@update:model-value="emitUpdate"
|
||||
class="config-field"
|
||||
@update:model-value="emitUpdate"
|
||||
/>
|
||||
|
||||
<ObjectEditor
|
||||
v-else-if="itemMeta?.type === 'dict'"
|
||||
:model-value="modelValue"
|
||||
:item-meta="itemMeta"
|
||||
@update:model-value="emitUpdate"
|
||||
class="config-field"
|
||||
@update:model-value="emitUpdate"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-else
|
||||
:model-value="modelValue"
|
||||
@update:model-value="emitUpdate"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="config-field"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
@update:model-value="emitUpdate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -6,23 +6,50 @@ import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="filter-controls mb-2" v-if="showLevelBtns">
|
||||
<v-chip-group v-model="selectedLevels" column multiple>
|
||||
<v-chip v-for="level in logLevels" :key="level" :color="getLevelColor(level)" filter variant="flat" size="small"
|
||||
:text-color="level === 'DEBUG' || level === 'INFO' ? 'black' : 'white'" class="font-weight-medium">
|
||||
<div
|
||||
v-if="showLevelBtns"
|
||||
class="filter-controls mb-2"
|
||||
>
|
||||
<v-chip-group
|
||||
v-model="selectedLevels"
|
||||
column
|
||||
multiple
|
||||
>
|
||||
<v-chip
|
||||
v-for="level in logLevels"
|
||||
:key="level"
|
||||
:color="getLevelColor(level)"
|
||||
filter
|
||||
variant="flat"
|
||||
size="small"
|
||||
:text-color="level === 'DEBUG' || level === 'INFO' ? 'black' : 'white'"
|
||||
class="font-weight-medium"
|
||||
>
|
||||
{{ level }}
|
||||
</v-chip>
|
||||
</v-chip-group>
|
||||
</div>
|
||||
|
||||
<div id="term" style="background-color: #1e1e1e; padding: 16px; border-radius: 8px; overflow-y:auto; height: 100%">
|
||||
</div>
|
||||
<div
|
||||
id="term"
|
||||
style="background-color: #1e1e1e; padding: 16px; border-radius: 8px; overflow-y:auto; height: 100%"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ConsoleDisplayer',
|
||||
props: {
|
||||
historyNum: {
|
||||
type: String,
|
||||
default: "-1"
|
||||
},
|
||||
showLevelBtns: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
autoScroll: true,
|
||||
@@ -59,16 +86,6 @@ export default {
|
||||
return useCommonStore();
|
||||
},
|
||||
},
|
||||
props: {
|
||||
historyNum: {
|
||||
type: String,
|
||||
default: "-1"
|
||||
},
|
||||
showLevelBtns: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedLevels: {
|
||||
handler() {
|
||||
|
||||
@@ -165,13 +165,16 @@ const viewChangelog = () => {
|
||||
location="top"
|
||||
:text="
|
||||
extension.display_name?.length &&
|
||||
extension.display_name !== extension.name
|
||||
extension.display_name !== extension.name
|
||||
? `${extension.display_name} (${extension.name})`
|
||||
: extension.name
|
||||
"
|
||||
>
|
||||
<template v-slot:activator="{ props: titleTooltipProps }">
|
||||
<span v-bind="titleTooltipProps" class="extension-title__text">{{
|
||||
<template #activator="{ props: titleTooltipProps }">
|
||||
<span
|
||||
v-bind="titleTooltipProps"
|
||||
class="extension-title__text"
|
||||
>{{
|
||||
extension.display_name?.length
|
||||
? extension.display_name
|
||||
: extension.name
|
||||
@@ -179,10 +182,10 @@ const viewChangelog = () => {
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip
|
||||
location="top"
|
||||
v-if="extension?.has_update && !marketMode"
|
||||
location="top"
|
||||
>
|
||||
<template v-slot:activator="{ props: tooltipProps }">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-icon
|
||||
v-bind="tooltipProps"
|
||||
color="warning"
|
||||
@@ -191,19 +194,21 @@ const viewChangelog = () => {
|
||||
size="small"
|
||||
style="cursor: pointer"
|
||||
@click.stop="updateExtension"
|
||||
></v-icon>
|
||||
/>
|
||||
</template>
|
||||
<span
|
||||
>{{ tm("card.status.hasUpdate") }}:
|
||||
{{ extension.online_version }}</span
|
||||
>
|
||||
<span>{{ tm("card.status.hasUpdate") }}:
|
||||
{{ extension.online_version }}</span>
|
||||
</v-tooltip>
|
||||
</p>
|
||||
|
||||
<template v-if="!marketMode">
|
||||
<v-tooltip location="left">
|
||||
<template v-slot:activator="{ props: tooltipProps }">
|
||||
<div v-bind="tooltipProps" class="extension-switch-wrap" @click.stop>
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<div
|
||||
v-bind="tooltipProps"
|
||||
class="extension-switch-wrap"
|
||||
@click.stop
|
||||
>
|
||||
<v-switch
|
||||
:model-value="extension.activated"
|
||||
color="success"
|
||||
@@ -211,7 +216,7 @@ const viewChangelog = () => {
|
||||
hide-details
|
||||
inset
|
||||
@update:model-value="toggleActivation"
|
||||
></v-switch>
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<span>{{
|
||||
@@ -222,27 +227,32 @@ const viewChangelog = () => {
|
||||
<template v-else>
|
||||
<div class="extension-market-menu-wrap">
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ props: menuProps }">
|
||||
<template #activator="{ props: menuProps }">
|
||||
<v-btn
|
||||
v-if="extension?.repo"
|
||||
icon
|
||||
variant="text"
|
||||
aria-label="more"
|
||||
v-if="extension?.repo"
|
||||
:href="extension?.repo"
|
||||
target="_blank"
|
||||
>
|
||||
<v-icon icon="mdi-github"></v-icon>
|
||||
<v-icon icon="mdi-github" />
|
||||
</v-btn>
|
||||
<v-btn v-bind="menuProps" icon variant="text" aria-label="more">
|
||||
<v-icon icon="mdi-dots-vertical"></v-icon>
|
||||
<v-btn
|
||||
v-bind="menuProps"
|
||||
icon
|
||||
variant="text"
|
||||
aria-label="more"
|
||||
>
|
||||
<v-icon icon="mdi-dots-vertical" />
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-list>
|
||||
<v-list-item @click="viewReadme">
|
||||
<v-list-item-title
|
||||
>📄 {{ tm("buttons.viewDocs") }}</v-list-item-title
|
||||
>
|
||||
<v-list-item-title>
|
||||
📄 {{ tm("buttons.viewDocs") }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
@@ -250,14 +260,16 @@ const viewChangelog = () => {
|
||||
@click="installExtension"
|
||||
>
|
||||
<v-list-item-title>
|
||||
{{ tm("buttons.install") }}</v-list-item-title
|
||||
>
|
||||
{{ tm("buttons.install") }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="marketMode && extension?.installed">
|
||||
<v-list-item-title class="text--disabled">{{
|
||||
tm("status.installed")
|
||||
}}</v-list-item-title>
|
||||
<v-list-item-title class="text--disabled">
|
||||
{{
|
||||
tm("status.installed")
|
||||
}}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
@@ -272,13 +284,20 @@ const viewChangelog = () => {
|
||||
:alt="extension.name"
|
||||
class="extension-logo"
|
||||
@error="logoLoadFailed = true"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="extension-meta-group">
|
||||
<div class="extension-chip-group d-flex flex-wrap">
|
||||
<v-chip color="primary" label size="small">
|
||||
<v-icon icon="mdi-source-branch" start></v-icon>
|
||||
<v-chip
|
||||
color="primary"
|
||||
label
|
||||
size="small"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-source-branch"
|
||||
start
|
||||
/>
|
||||
{{ extension.version }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
@@ -289,7 +308,10 @@ const viewChangelog = () => {
|
||||
style="cursor: pointer"
|
||||
@click="updateExtension"
|
||||
>
|
||||
<v-icon icon="mdi-arrow-up-bold" start></v-icon>
|
||||
<v-icon
|
||||
icon="mdi-arrow-up-bold"
|
||||
start
|
||||
/>
|
||||
{{ extension.online_version }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
@@ -297,10 +319,13 @@ const viewChangelog = () => {
|
||||
color="primary"
|
||||
label
|
||||
size="small"
|
||||
@click="viewHandlers"
|
||||
style="cursor: pointer"
|
||||
@click="viewHandlers"
|
||||
>
|
||||
<v-icon icon="mdi-cogs" start></v-icon>
|
||||
<v-icon
|
||||
icon="mdi-cogs"
|
||||
start
|
||||
/>
|
||||
{{ extension.handlers?.length
|
||||
}}{{ tm("card.status.handlersCount") }}
|
||||
</v-chip>
|
||||
@@ -337,11 +362,17 @@ const viewChangelog = () => {
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="extension-actions" @click.stop>
|
||||
<v-card-actions
|
||||
class="extension-actions"
|
||||
@click.stop
|
||||
>
|
||||
<template v-if="!marketMode">
|
||||
<v-spacer></v-spacer>
|
||||
<v-tooltip location="top" :text="tm('buttons.viewDocs')">
|
||||
<template v-slot:activator="{ props: actionProps }">
|
||||
<v-spacer />
|
||||
<v-tooltip
|
||||
location="top"
|
||||
:text="tm('buttons.viewDocs')"
|
||||
>
|
||||
<template #activator="{ props: actionProps }">
|
||||
<v-btn
|
||||
v-bind="actionProps"
|
||||
icon="mdi-book-open-page-variant"
|
||||
@@ -349,12 +380,15 @@ const viewChangelog = () => {
|
||||
variant="tonal"
|
||||
color="info"
|
||||
@click="viewReadme"
|
||||
></v-btn>
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip location="top" :text="tm('card.actions.pluginConfig')">
|
||||
<template v-slot:activator="{ props: actionProps }">
|
||||
<v-tooltip
|
||||
location="top"
|
||||
:text="tm('card.actions.pluginConfig')"
|
||||
>
|
||||
<template #activator="{ props: actionProps }">
|
||||
<v-btn
|
||||
v-bind="actionProps"
|
||||
icon="mdi-cog"
|
||||
@@ -362,12 +396,16 @@ const viewChangelog = () => {
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
@click="configure"
|
||||
></v-btn>
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip v-if="extension?.repo" location="top" :text="tm('buttons.viewRepo')">
|
||||
<template v-slot:activator="{ props: actionProps }">
|
||||
<v-tooltip
|
||||
v-if="extension?.repo"
|
||||
location="top"
|
||||
:text="tm('buttons.viewRepo')"
|
||||
>
|
||||
<template #activator="{ props: actionProps }">
|
||||
<v-btn
|
||||
v-bind="actionProps"
|
||||
icon="mdi-github"
|
||||
@@ -376,12 +414,15 @@ const viewChangelog = () => {
|
||||
color="secondary"
|
||||
:href="extension.repo"
|
||||
target="_blank"
|
||||
></v-btn>
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip location="top" :text="tm('card.actions.reloadPlugin')">
|
||||
<template v-slot:activator="{ props: actionProps }">
|
||||
<v-tooltip
|
||||
location="top"
|
||||
:text="tm('card.actions.reloadPlugin')"
|
||||
>
|
||||
<template #activator="{ props: actionProps }">
|
||||
<v-btn
|
||||
v-bind="actionProps"
|
||||
icon="mdi-refresh"
|
||||
@@ -389,11 +430,14 @@ const viewChangelog = () => {
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
@click="reloadExtension"
|
||||
></v-btn>
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<StyledMenu location="top end" offset="8">
|
||||
<StyledMenu
|
||||
location="top end"
|
||||
offset="8"
|
||||
>
|
||||
<template #activator="{ props: menuProps }">
|
||||
<v-btn
|
||||
v-bind="menuProps"
|
||||
@@ -401,28 +445,48 @@ const viewChangelog = () => {
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
></v-btn>
|
||||
/>
|
||||
</template>
|
||||
|
||||
<v-list-item class="styled-menu-item" prepend-icon="mdi-information" @click="viewHandlers">
|
||||
<v-list-item
|
||||
class="styled-menu-item"
|
||||
prepend-icon="mdi-information"
|
||||
@click="viewHandlers"
|
||||
>
|
||||
<v-list-item-title>{{ tm("buttons.viewInfo") }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item class="styled-menu-item" prepend-icon="mdi-update" @click="updateExtension">
|
||||
<v-list-item-title>{{
|
||||
extension.has_update
|
||||
? tm("card.actions.updateTo") + " " + extension.online_version
|
||||
: tm("card.actions.reinstall")
|
||||
}}</v-list-item-title>
|
||||
<v-list-item
|
||||
class="styled-menu-item"
|
||||
prepend-icon="mdi-update"
|
||||
@click="updateExtension"
|
||||
>
|
||||
<v-list-item-title>
|
||||
{{
|
||||
extension.has_update
|
||||
? tm("card.actions.updateTo") + " " + extension.online_version
|
||||
: tm("card.actions.reinstall")
|
||||
}}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item class="styled-menu-item" prepend-icon="mdi-delete" @click="uninstallExtension">
|
||||
<v-list-item-title class="text-error">{{ tm("card.actions.uninstallPlugin") }}</v-list-item-title>
|
||||
<v-list-item
|
||||
class="styled-menu-item"
|
||||
prepend-icon="mdi-delete"
|
||||
@click="uninstallExtension"
|
||||
>
|
||||
<v-list-item-title class="text-error">
|
||||
{{ tm("card.actions.uninstallPlugin") }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</StyledMenu>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-btn color="primary" size="small" @click="viewReadme">
|
||||
<v-btn
|
||||
color="primary"
|
||||
size="small"
|
||||
@click="viewReadme"
|
||||
>
|
||||
{{ tm("buttons.viewDocs") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<div class="file-config-item">
|
||||
<div class="d-flex align-center gap-2">
|
||||
<v-btn size="small" color="primary" variant="tonal" @click="dialog = true">
|
||||
<v-btn
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
@click="dialog = true"
|
||||
>
|
||||
{{ tm('fileUpload.button') }}
|
||||
</v-btn>
|
||||
<span class="text-caption text-medium-emphasis ml-2">
|
||||
@@ -9,61 +14,123 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<v-dialog v-model="dialog" max-width="700">
|
||||
<v-card class="file-dialog-card" variant="flat">
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
max-width="700"
|
||||
>
|
||||
<v-card
|
||||
class="file-dialog-card"
|
||||
variant="flat"
|
||||
>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<span class="text-h3">{{ tm('fileUpload.dialogTitle') }}</span>
|
||||
<v-spacer />
|
||||
<v-btn icon="mdi-close" variant="text" @click="dialog = false" />
|
||||
<v-btn
|
||||
icon="mdi-close"
|
||||
variant="text"
|
||||
@click="dialog = false"
|
||||
/>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="file-dialog-body">
|
||||
<div v-if="mergedFileItems.length === 0" class="empty-text">
|
||||
<div
|
||||
v-if="mergedFileItems.length === 0"
|
||||
class="empty-text"
|
||||
>
|
||||
{{ tm('fileUpload.empty') }}
|
||||
</div>
|
||||
|
||||
<v-list density="compact" lines="one">
|
||||
<v-list-item v-for="item in mergedFileItems" :key="item.path">
|
||||
<v-list
|
||||
density="compact"
|
||||
lines="one"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="item in mergedFileItems"
|
||||
:key="item.path"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon size="18">mdi-file</v-icon>
|
||||
<v-icon size="18">
|
||||
mdi-file
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title class="file-name">
|
||||
{{ getDisplayName(item.path) }}
|
||||
</v-list-item-title>
|
||||
<template #append>
|
||||
<div class="d-flex align-center gap-1">
|
||||
<v-chip v-if="item.status !== 'ok'" size="x-small" :color="getStatusColor(item.status)"
|
||||
variant="tonal">
|
||||
<v-chip
|
||||
v-if="item.status !== 'ok'"
|
||||
size="x-small"
|
||||
:color="getStatusColor(item.status)"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ getStatusText(item.status) }}
|
||||
</v-chip>
|
||||
<v-btn v-if="item.status === 'unconfigured'" icon="mdi-plus" size="x-small" variant="text"
|
||||
@click="addToConfig(item.path)" />
|
||||
<v-btn icon="mdi-delete" size="x-small" variant="text"
|
||||
@click="item.status === 'unconfigured' ? deletePhysicalFile(item.path) : deleteFile(item.path)" />
|
||||
<v-btn
|
||||
v-if="item.status === 'unconfigured'"
|
||||
icon="mdi-plus"
|
||||
size="x-small"
|
||||
variant="text"
|
||||
@click="addToConfig(item.path)"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
size="x-small"
|
||||
variant="text"
|
||||
@click="item.status === 'unconfigured' ? deletePhysicalFile(item.path) : deleteFile(item.path)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider v-if="mergedFileItems.length > 0" class="my-2" />
|
||||
<v-divider
|
||||
v-if="mergedFileItems.length > 0"
|
||||
class="my-2"
|
||||
/>
|
||||
|
||||
<v-list-item class="upload-item" :class="{ dragover: isDragging }" @drop.prevent="handleDrop"
|
||||
@dragover.prevent="isDragging = true" @dragleave="isDragging = false" @click="openFilePicker">
|
||||
<v-list-item
|
||||
class="upload-item"
|
||||
:class="{ dragover: isDragging }"
|
||||
@drop.prevent="handleDrop"
|
||||
@dragover.prevent="isDragging = true"
|
||||
@dragleave="isDragging = false"
|
||||
@click="openFilePicker"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon size="18" color="primary">mdi-plus</v-icon>
|
||||
<v-icon
|
||||
size="18"
|
||||
color="primary"
|
||||
>
|
||||
mdi-plus
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ tm('fileUpload.dropzone') }}</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="allowedTypesText" class="upload-hint">
|
||||
<v-list-item-subtitle
|
||||
v-if="allowedTypesText"
|
||||
class="upload-hint"
|
||||
>
|
||||
{{ tm('fileUpload.allowedTypes', { types: allowedTypesText }) }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<input ref="fileInput" type="file" multiple hidden :accept="acceptAttr" @change="handleFileSelect" />
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
multiple
|
||||
hidden
|
||||
:accept="acceptAttr"
|
||||
@change="handleFileSelect"
|
||||
>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="file-dialog-actions">
|
||||
<v-spacer />
|
||||
<v-btn color="primary" variant="elevated" @click="dialog = false">
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
@click="dialog = false"
|
||||
>
|
||||
{{ tm('fileUpload.done') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
<template>
|
||||
<v-card class="item-card hover-elevation" style="padding: 4px;" elevation="0">
|
||||
<v-card
|
||||
class="item-card hover-elevation"
|
||||
style="padding: 4px;"
|
||||
elevation="0"
|
||||
>
|
||||
<v-card-title class="d-flex justify-space-between align-center pb-1 pt-3">
|
||||
<span class="text-h2 text-truncate" :title="getItemTitle()">{{ getItemTitle() }}</span>
|
||||
<span
|
||||
class="text-h2 text-truncate"
|
||||
:title="getItemTitle()"
|
||||
>{{ getItemTitle() }}</span>
|
||||
<v-tooltip location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
<template #activator="{ props }">
|
||||
<v-switch
|
||||
color="primary"
|
||||
hide-details
|
||||
@@ -13,38 +20,41 @@
|
||||
:disabled="loading || disableToggle"
|
||||
v-bind="props"
|
||||
@update:model-value="toggleEnabled"
|
||||
></v-switch>
|
||||
/>
|
||||
</template>
|
||||
<span>{{ getItemEnabled() ? t('core.common.itemCard.enabled') : t('core.common.itemCard.disabled') }}</span>
|
||||
</v-tooltip>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<slot name="item-details" :item="item"></slot>
|
||||
<slot
|
||||
name="item-details"
|
||||
:item="item"
|
||||
/>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions style="margin: 8px;">
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
color="error"
|
||||
size="small"
|
||||
rounded="xl"
|
||||
:disabled="loading || disableDelete"
|
||||
@click="$emit('delete', item)"
|
||||
>
|
||||
{{ t('core.common.itemCard.delete') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="showEditButton"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
size="small"
|
||||
rounded="xl"
|
||||
:disabled="loading"
|
||||
@click="$emit('edit', item)"
|
||||
>
|
||||
{{ t('core.common.itemCard.edit') }}
|
||||
</v-btn>
|
||||
<v-card-actions style="margin: 8px;">
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
color="error"
|
||||
size="small"
|
||||
rounded="xl"
|
||||
:disabled="loading || disableDelete"
|
||||
@click="$emit('delete', item)"
|
||||
>
|
||||
{{ t('core.common.itemCard.delete') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="showEditButton"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
size="small"
|
||||
rounded="xl"
|
||||
:disabled="loading"
|
||||
@click="$emit('edit', item)"
|
||||
>
|
||||
{{ t('core.common.itemCard.edit') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="showCopyButton"
|
||||
variant="tonal"
|
||||
@@ -56,17 +66,24 @@
|
||||
>
|
||||
{{ t('core.common.itemCard.copy') }}
|
||||
</v-btn>
|
||||
<slot name="actions" :item="item"></slot>
|
||||
<v-spacer></v-spacer>
|
||||
<slot
|
||||
name="actions"
|
||||
:item="item"
|
||||
/>
|
||||
<v-spacer />
|
||||
</v-card-actions>
|
||||
|
||||
<div class="d-flex justify-end align-center" style="position: absolute; bottom: 16px; right: 16px; opacity: 0.2;" v-if="bglogo">
|
||||
<div
|
||||
v-if="bglogo"
|
||||
class="d-flex justify-end align-center"
|
||||
style="position: absolute; bottom: 16px; right: 16px; opacity: 0.2;"
|
||||
>
|
||||
<v-img
|
||||
:src="bglogo"
|
||||
contain
|
||||
width="120"
|
||||
height="120"
|
||||
></v-img>
|
||||
/>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
@@ -76,10 +93,6 @@ import { useI18n } from '@/i18n/composables';
|
||||
|
||||
export default {
|
||||
name: 'ItemCard',
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
return { t };
|
||||
},
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
@@ -119,6 +132,10 @@ export default {
|
||||
}
|
||||
},
|
||||
emits: ['toggle-enabled', 'delete', 'edit', 'copy'],
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
return { t };
|
||||
},
|
||||
methods: {
|
||||
getItemTitle() {
|
||||
return this.item[this.titleField];
|
||||
|
||||
@@ -1,20 +1,47 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row v-if="items.length === 0">
|
||||
<v-col cols="12" class="text-center pa-8">
|
||||
<v-icon size="64" color="grey-lighten-1">{{ emptyIcon }}</v-icon>
|
||||
<p class="text-grey mt-4">{{ displayEmptyText }}</p>
|
||||
<v-col
|
||||
cols="12"
|
||||
class="text-center pa-8"
|
||||
>
|
||||
<v-icon
|
||||
size="64"
|
||||
color="grey-lighten-1"
|
||||
>
|
||||
{{ emptyIcon }}
|
||||
</v-icon>
|
||||
<p class="text-grey mt-4">
|
||||
{{ displayEmptyText }}
|
||||
</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-else>
|
||||
<v-col v-for="(item, index) in items" :key="index" cols="12" md="6" lg="4" xl="3">
|
||||
<v-card class="item-card hover-elevation" style="padding: 4px;" elevation="0">
|
||||
<div class="item-status-indicator" :class="{'active': getItemEnabled(item)}"></div>
|
||||
<v-col
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
cols="12"
|
||||
md="6"
|
||||
lg="4"
|
||||
xl="3"
|
||||
>
|
||||
<v-card
|
||||
class="item-card hover-elevation"
|
||||
style="padding: 4px;"
|
||||
elevation="0"
|
||||
>
|
||||
<div
|
||||
class="item-status-indicator"
|
||||
:class="{'active': getItemEnabled(item)}"
|
||||
/>
|
||||
<v-card-title class="d-flex justify-space-between align-center pb-1 pt-3">
|
||||
<span class="text-h2 text-truncate" :title="getItemTitle(item)">{{ getItemTitle(item) }}</span>
|
||||
<span
|
||||
class="text-h2 text-truncate"
|
||||
:title="getItemTitle(item)"
|
||||
>{{ getItemTitle(item) }}</span>
|
||||
<v-tooltip location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
<template #activator="{ props }">
|
||||
<v-switch
|
||||
color="primary"
|
||||
hide-details
|
||||
@@ -22,14 +49,17 @@
|
||||
:model-value="getItemEnabled(item)"
|
||||
v-bind="props"
|
||||
@update:model-value="toggleEnabled(item)"
|
||||
></v-switch>
|
||||
/>
|
||||
</template>
|
||||
<span>{{ getItemEnabled(item) ? t('core.common.itemCard.enabled') : t('core.common.itemCard.disabled') }}</span>
|
||||
</v-tooltip>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<slot name="item-details" :item="item"></slot>
|
||||
<slot
|
||||
name="item-details"
|
||||
:item="item"
|
||||
/>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions style="margin: 8px;">
|
||||
@@ -49,19 +79,22 @@
|
||||
>
|
||||
{{ t('core.common.itemCard.edit') }}
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
</v-card-actions>
|
||||
|
||||
<div class="d-flex justify-end align-center" style="position: absolute; bottom: 16px; right: 16px; opacity: 0.2;" v-if="bglogo">
|
||||
<div
|
||||
v-if="bglogo"
|
||||
class="d-flex justify-end align-center"
|
||||
style="position: absolute; bottom: 16px; right: 16px; opacity: 0.2;"
|
||||
>
|
||||
<v-img
|
||||
:src="bglogo"
|
||||
contain
|
||||
width="120"
|
||||
height="120"
|
||||
class="rounded-circle"
|
||||
></v-img>
|
||||
/>
|
||||
</div>
|
||||
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -73,10 +106,6 @@ import { useI18n } from '@/i18n/composables';
|
||||
|
||||
export default {
|
||||
name: 'ItemCardGrid',
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
return { t };
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
@@ -104,6 +133,10 @@ export default {
|
||||
}
|
||||
},
|
||||
emits: ['toggle-enabled', 'delete', 'edit'],
|
||||
setup() {
|
||||
const { t } = useI18n();
|
||||
return { t };
|
||||
},
|
||||
computed: {
|
||||
displayEmptyText() {
|
||||
return this.emptyText || this.t('core.common.itemCard.noData');
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
<template>
|
||||
<div class="d-flex align-center justify-space-between" style="gap: 8px;">
|
||||
<div
|
||||
class="d-flex align-center justify-space-between"
|
||||
style="gap: 8px;"
|
||||
>
|
||||
<div style="flex: 1; min-width: 0; overflow: hidden;">
|
||||
<span v-if="!modelValue || (Array.isArray(modelValue) && modelValue.length === 0)"
|
||||
style="color: rgb(var(--v-theme-primaryText));">
|
||||
<span
|
||||
v-if="!modelValue || (Array.isArray(modelValue) && modelValue.length === 0)"
|
||||
style="color: rgb(var(--v-theme-primaryText));"
|
||||
>
|
||||
{{ tm('knowledgeBaseSelector.notSelected') }}
|
||||
</span>
|
||||
<div v-else class="d-flex flex-wrap gap-1">
|
||||
<div
|
||||
v-else
|
||||
class="d-flex flex-wrap gap-1"
|
||||
>
|
||||
<v-chip
|
||||
v-for="name in modelValue"
|
||||
:key="name"
|
||||
@@ -13,39 +21,66 @@
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
closable
|
||||
style="max-width: 100%;"
|
||||
@click:close="removeKnowledgeBase(name)"
|
||||
style="max-width: 100%;">
|
||||
<span class="text-truncate" style="max-width: 200px;">{{ name }}</span>
|
||||
>
|
||||
<span
|
||||
class="text-truncate"
|
||||
style="max-width: 200px;"
|
||||
>{{ name }}</span>
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
<v-btn size="small" color="primary" variant="tonal" @click="openDialog" style="flex-shrink: 0;">
|
||||
<v-btn
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
style="flex-shrink: 0;"
|
||||
@click="openDialog"
|
||||
>
|
||||
{{ buttonText || tm('knowledgeBaseSelector.buttonText') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- Knowledge Base Selection Dialog -->
|
||||
<v-dialog v-model="dialog" max-width="600px">
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
max-width="600px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
|
||||
<v-card-title
|
||||
class="text-h3 py-4"
|
||||
style="font-weight: normal;"
|
||||
>
|
||||
{{ tm('knowledgeBaseSelector.dialogTitle') }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-0" style="max-height: 400px; overflow-y: auto;">
|
||||
<v-progress-linear v-if="loading" indeterminate color="primary"></v-progress-linear>
|
||||
<v-card-text
|
||||
class="pa-0"
|
||||
style="max-height: 400px; overflow-y: auto;"
|
||||
>
|
||||
<v-progress-linear
|
||||
v-if="loading"
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
|
||||
<!-- 知识库列表 -->
|
||||
<v-list v-if="!loading" density="compact">
|
||||
<v-list
|
||||
v-if="!loading"
|
||||
density="compact"
|
||||
>
|
||||
<!-- 知识库选项 -->
|
||||
<v-list-item
|
||||
v-for="kb in knowledgeBaseList"
|
||||
:key="kb.kb_id"
|
||||
:value="kb.kb_name"
|
||||
@click="selectKnowledgeBase(kb.kb_name)"
|
||||
:active="isSelected(kb.kb_name)"
|
||||
rounded="md"
|
||||
class="ma-1">
|
||||
<template v-slot:prepend>
|
||||
class="ma-1"
|
||||
@click="selectKnowledgeBase(kb.kb_name)"
|
||||
>
|
||||
<template #prepend>
|
||||
<span class="emoji-icon">{{ kb.emoji || '📚' }}</span>
|
||||
</template>
|
||||
<v-list-item-title>{{ kb.kb_name }}</v-list-item-title>
|
||||
@@ -55,21 +90,41 @@
|
||||
<span v-if="kb.chunk_count !== undefined"> - {{ tm('knowledgeBaseSelector.chunkCount', { count: kb.chunk_count }) }}</span>
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<template v-slot:append>
|
||||
<v-icon v-if="isSelected(kb.kb_name)" color="primary">
|
||||
<template #append>
|
||||
<v-icon
|
||||
v-if="isSelected(kb.kb_name)"
|
||||
color="primary"
|
||||
>
|
||||
mdi-checkbox-marked
|
||||
</v-icon>
|
||||
<v-icon v-else color="grey-lighten-1">
|
||||
<v-icon
|
||||
v-else
|
||||
color="grey-lighten-1"
|
||||
>
|
||||
mdi-checkbox-blank-outline
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<!-- 当没有知识库时显示创建提示 -->
|
||||
<div v-if="knowledgeBaseList.length === 0" class="text-center py-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-database-off</v-icon>
|
||||
<p class="text-grey mt-4 mb-4">{{ tm('knowledgeBaseSelector.noKnowledgeBases') }}</p>
|
||||
<v-btn color="primary" variant="tonal" @click="goToKnowledgeBasePage">
|
||||
<div
|
||||
v-if="knowledgeBaseList.length === 0"
|
||||
class="text-center py-8"
|
||||
>
|
||||
<v-icon
|
||||
size="64"
|
||||
color="grey-lighten-1"
|
||||
>
|
||||
mdi-database-off
|
||||
</v-icon>
|
||||
<p class="text-grey mt-4 mb-4">
|
||||
{{ tm('knowledgeBaseSelector.noKnowledgeBases') }}
|
||||
</p>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
@click="goToKnowledgeBasePage"
|
||||
>
|
||||
{{ tm('knowledgeBaseSelector.createKnowledgeBase') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
@@ -77,14 +132,23 @@
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<div v-if="selectedKnowledgeBases.length > 0" class="text-caption text-grey">
|
||||
<div
|
||||
v-if="selectedKnowledgeBases.length > 0"
|
||||
class="text-caption text-grey"
|
||||
>
|
||||
{{ tm('knowledgeBaseSelector.selectedCount', { count: selectedKnowledgeBases.length }) }}
|
||||
</div>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="cancelSelection">{{ tm('knowledgeBaseSelector.cancelSelection') }}</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="cancelSelection"
|
||||
>
|
||||
{{ tm('knowledgeBaseSelector.cancelSelection') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="confirmSelection">
|
||||
@click="confirmSelection"
|
||||
>
|
||||
{{ tm('knowledgeBaseSelector.confirmSelection') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<StyledMenu offset="12" location="bottom center">
|
||||
<template v-slot:activator="{ props: activatorProps }">
|
||||
<StyledMenu
|
||||
offset="12"
|
||||
location="bottom center"
|
||||
>
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-btn
|
||||
v-bind="activatorProps"
|
||||
:variant="(props.variant === 'header' || props.variant === 'chatbox') ? 'flat' : 'text'"
|
||||
@@ -16,7 +19,10 @@
|
||||
>
|
||||
mdi-translate
|
||||
</v-icon>
|
||||
<v-tooltip activator="parent" location="top">
|
||||
<v-tooltip
|
||||
activator="parent"
|
||||
location="top"
|
||||
>
|
||||
{{ t('core.common.language') }}
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
@@ -26,12 +32,12 @@
|
||||
v-for="lang in languages"
|
||||
:key="lang.code"
|
||||
:value="lang.code"
|
||||
@click="changeLanguage(lang.code)"
|
||||
:class="{ 'styled-menu-item-active': currentLocale === lang.code }"
|
||||
class="styled-menu-item"
|
||||
rounded="md"
|
||||
@click="changeLanguage(lang.code)"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<template #prepend>
|
||||
<span class="language-flag">{{ lang.flag }}</span>
|
||||
</template>
|
||||
<v-list-item-title>{{ lang.name }}</v-list-item-title>
|
||||
|
||||
@@ -1,36 +1,67 @@
|
||||
<template>
|
||||
<div class="d-flex align-center justify-space-between ga-2">
|
||||
<div v-if="isSingleItemMode" class="flex-grow-1 d-flex align-center ga-2">
|
||||
<div
|
||||
v-if="isSingleItemMode"
|
||||
class="flex-grow-1 d-flex align-center ga-2"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="singleItemValue"
|
||||
hide-details
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
class="flex-grow-1"
|
||||
></v-text-field>
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span v-if="!modelValue || modelValue.length === 0" style="color: rgb(var(--v-theme-primaryText));">
|
||||
<span
|
||||
v-if="!modelValue || modelValue.length === 0"
|
||||
style="color: rgb(var(--v-theme-primaryText));"
|
||||
>
|
||||
{{ t('core.common.list.noItems') }}
|
||||
</span>
|
||||
<div v-else class="d-flex flex-wrap ga-2">
|
||||
<v-chip v-for="item in displayItems" :key="item" size="x-small" label color="primary">
|
||||
<div
|
||||
v-else
|
||||
class="d-flex flex-wrap ga-2"
|
||||
>
|
||||
<v-chip
|
||||
v-for="item in displayItems"
|
||||
:key="item"
|
||||
size="x-small"
|
||||
label
|
||||
color="primary"
|
||||
>
|
||||
{{ item.length > 20 ? item.slice(0, 20) + '...' : item }}
|
||||
</v-chip>
|
||||
<v-chip v-if="modelValue.length > maxDisplayItems" size="x-small" label color="grey-lighten-1">
|
||||
<v-chip
|
||||
v-if="modelValue.length > maxDisplayItems"
|
||||
size="x-small"
|
||||
label
|
||||
color="grey-lighten-1"
|
||||
>
|
||||
+{{ modelValue.length - maxDisplayItems }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
|
||||
<v-btn
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
@click="openDialog"
|
||||
>
|
||||
{{ preferSingleItem ? t('core.common.list.addMore') : (buttonText || t('core.common.list.modifyButton')) }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- List Management Dialog -->
|
||||
<v-dialog v-model="dialog" max-width="600px">
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
max-width="600px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
|
||||
<v-card-title
|
||||
class="text-h3 py-4"
|
||||
style="font-weight: normal;"
|
||||
>
|
||||
{{ dialogTitle || t('core.common.list.editTitle') }}
|
||||
</v-card-title>
|
||||
|
||||
@@ -40,42 +71,56 @@
|
||||
<v-text-field
|
||||
v-model="newItem"
|
||||
:label="t('core.common.list.addItemPlaceholder')"
|
||||
@keyup.enter="addItem"
|
||||
clearable
|
||||
hide-details
|
||||
variant="outlined"
|
||||
hide-details
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:placeholder="t('core.common.list.inputPlaceholder')"
|
||||
class="flex-grow-1">
|
||||
</v-text-field>
|
||||
:placeholder="t('core.common.list.inputPlaceholder')"
|
||||
class="flex-grow-1"
|
||||
@keyup.enter="addItem"
|
||||
/>
|
||||
<v-btn
|
||||
@click="addItem"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
size="small"
|
||||
:disabled="!newItem.trim()">
|
||||
:disabled="!newItem.trim()"
|
||||
@click="addItem"
|
||||
>
|
||||
{{ t('core.common.list.addButton') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@click="showBatchImport = true"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
size="small">
|
||||
<v-icon size="small">mdi-import</v-icon>
|
||||
color="primary"
|
||||
size="small"
|
||||
@click="showBatchImport = true"
|
||||
>
|
||||
<v-icon size="small">
|
||||
mdi-import
|
||||
</v-icon>
|
||||
{{ t('core.common.list.batchImport') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text class="pa-0" style="max-height: 400px; overflow-y: auto;">
|
||||
<v-list v-if="localItems.length > 0" density="compact">
|
||||
<v-card-text
|
||||
class="pa-0"
|
||||
style="max-height: 400px; overflow-y: auto;"
|
||||
>
|
||||
<v-list
|
||||
v-if="localItems.length > 0"
|
||||
density="compact"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(item, index) in localItems"
|
||||
:key="index"
|
||||
rounded="md"
|
||||
class="ma-1 list-item-clickable"
|
||||
@click="startEdit(index, item)">
|
||||
<v-list-item-title v-if="editIndex !== index" class="item-text">
|
||||
@click="startEdit(index, item)"
|
||||
>
|
||||
<v-list-item-title
|
||||
v-if="editIndex !== index"
|
||||
class="item-text"
|
||||
>
|
||||
{{ item }}
|
||||
</v-list-item-title>
|
||||
<v-text-field
|
||||
@@ -84,29 +129,31 @@
|
||||
hide-details
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
@keyup.enter="saveEdit"
|
||||
autofocus
|
||||
@keyup.enter="saveEdit"
|
||||
@keyup.esc="cancelEdit"
|
||||
@click.stop
|
||||
autofocus
|
||||
></v-text-field>
|
||||
/>
|
||||
|
||||
<template v-slot:append>
|
||||
<template #append>
|
||||
<div class="d-flex">
|
||||
<v-btn
|
||||
v-if="editIndex === index"
|
||||
@click.stop="saveEdit"
|
||||
variant="plain"
|
||||
color="success"
|
||||
icon
|
||||
size="small">
|
||||
size="small"
|
||||
@click.stop="saveEdit"
|
||||
>
|
||||
<v-icon>mdi-check</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@click.stop="editIndex === index ? cancelEdit() : removeItem(index)"
|
||||
variant="plain"
|
||||
:color="editIndex === index ? 'error' : 'default'"
|
||||
icon
|
||||
size="small">
|
||||
:color="editIndex === index ? 'error' : 'default'"
|
||||
icon
|
||||
size="small"
|
||||
@click.stop="editIndex === index ? cancelEdit() : removeItem(index)"
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
@@ -114,24 +161,50 @@
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<div v-else class="text-center py-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-format-list-bulleted</v-icon>
|
||||
<p class="text-grey mt-4">{{ t('core.common.list.noItemsHint') }}</p>
|
||||
<div
|
||||
v-else
|
||||
class="text-center py-8"
|
||||
>
|
||||
<v-icon
|
||||
size="64"
|
||||
color="grey-lighten-1"
|
||||
>
|
||||
mdi-format-list-bulleted
|
||||
</v-icon>
|
||||
<p class="text-grey mt-4">
|
||||
{{ t('core.common.list.noItemsHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="cancelDialog">{{ t('core.common.cancel') }}</v-btn>
|
||||
<v-btn color="primary" @click="confirmDialog">{{ t('core.common.confirm') }}</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="cancelDialog"
|
||||
>
|
||||
{{ t('core.common.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="confirmDialog"
|
||||
>
|
||||
{{ t('core.common.confirm') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Batch Import Dialog -->
|
||||
<v-dialog v-model="showBatchImport" max-width="600px">
|
||||
<v-dialog
|
||||
v-model="showBatchImport"
|
||||
max-width="600px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
|
||||
<v-card-title
|
||||
class="text-h3 py-4"
|
||||
style="font-weight: normal;"
|
||||
>
|
||||
{{ t('core.common.list.batchImportTitle') }}
|
||||
</v-card-title>
|
||||
|
||||
@@ -144,13 +217,21 @@
|
||||
variant="outlined"
|
||||
:hint="t('core.common.list.batchImportHint')"
|
||||
persistent-hint
|
||||
></v-textarea>
|
||||
/>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="cancelBatchImport">{{ t('core.common.cancel') }}</v-btn>
|
||||
<v-btn color="primary" @click="confirmBatchImport">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="cancelBatchImport"
|
||||
>
|
||||
{{ t('core.common.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="confirmBatchImport"
|
||||
>
|
||||
{{ t('core.common.list.batchImportButton', { count: batchImportPreviewCount }) }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
|
||||
@@ -2,16 +2,24 @@
|
||||
<div class="logo-container">
|
||||
<div class="logo-content">
|
||||
<div class="logo-image">
|
||||
<img width="110" src="@/assets/images/astrbot_logo_mini.webp" alt="AstrBot Logo">
|
||||
<img
|
||||
width="110"
|
||||
src="@/assets/images/astrbot_logo_mini.webp"
|
||||
alt="AstrBot Logo"
|
||||
>
|
||||
</div>
|
||||
<div class="logo-text">
|
||||
<h2
|
||||
:style="{ color: 'rgb(var(--v-theme-primary))' }"
|
||||
v-html="formatTitle(title || t('core.header.logoTitle'))"
|
||||
></h2>
|
||||
/>
|
||||
<!-- 父子组件传递css变量可能会出错,暂时使用十六进制颜色值 -->
|
||||
<h4 :style="{ color: 'rgba(var(--v-theme-on-surface), 0.72)' }"
|
||||
class="hint-text">{{ subtitle || t('core.header.accountDialog.title') }}</h4>
|
||||
<h4
|
||||
:style="{ color: 'rgba(var(--v-theme-on-surface), 0.72)' }"
|
||||
class="hint-text"
|
||||
>
|
||||
{{ subtitle || t('core.header.accountDialog.title') }}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,112 +1,208 @@
|
||||
<template>
|
||||
<v-dialog v-model="isOpen" persistent max-width="600" max-height="80vh" scrollable>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
{{ t('features.migration.dialog.title') }}
|
||||
</v-card-title>
|
||||
<v-dialog
|
||||
v-model="isOpen"
|
||||
persistent
|
||||
max-width="600"
|
||||
max-height="80vh"
|
||||
scrollable
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
{{ t('features.migration.dialog.title') }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-6">
|
||||
<p class="mb-4">{{ t('features.migration.dialog.warning') }}</p>
|
||||
<v-card-text class="pa-6">
|
||||
<p class="mb-4">
|
||||
{{ t('features.migration.dialog.warning') }}
|
||||
</p>
|
||||
|
||||
|
||||
<div v-if="migrationCompleted" class="text-center py-8">
|
||||
<v-icon size="64" color="success" class="mb-4">mdi-check-circle</v-icon>
|
||||
<h3 class="mb-4">{{ t('features.migration.dialog.completed') }}</h3>
|
||||
<p class="mb-4">{{ migrationResult?.message || t('features.migration.dialog.success') }}</p>
|
||||
<v-alert type="info" variant="tonal" class="mb-4">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-information</v-icon>
|
||||
</template>
|
||||
{{ t('features.migration.dialog.restartRecommended') }}
|
||||
</v-alert>
|
||||
</div>
|
||||
<div
|
||||
v-if="migrationCompleted"
|
||||
class="text-center py-8"
|
||||
>
|
||||
<v-icon
|
||||
size="64"
|
||||
color="success"
|
||||
class="mb-4"
|
||||
>
|
||||
mdi-check-circle
|
||||
</v-icon>
|
||||
<h3 class="mb-4">
|
||||
{{ t('features.migration.dialog.completed') }}
|
||||
</h3>
|
||||
<p class="mb-4">
|
||||
{{ migrationResult?.message || t('features.migration.dialog.success') }}
|
||||
</p>
|
||||
<v-alert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-information</v-icon>
|
||||
</template>
|
||||
{{ t('features.migration.dialog.restartRecommended') }}
|
||||
</v-alert>
|
||||
</div>
|
||||
|
||||
<div v-else-if="migrating" class="migration-in-progress">
|
||||
<div class="text-center py-4">
|
||||
<v-progress-circular indeterminate color="primary" class="mb-4"></v-progress-circular>
|
||||
<h3 class="mb-4">{{ t('features.migration.dialog.migrating') }}</h3>
|
||||
<p class="mb-4">{{ t('features.migration.dialog.migratingSubtitle') }}</p>
|
||||
</div>
|
||||
<div class="console-container">
|
||||
<ConsoleDisplayer ref="consoleDisplayer" :showLevelBtns="false" style="height: 300px;" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="migrating"
|
||||
class="migration-in-progress"
|
||||
>
|
||||
<div class="text-center py-4">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
class="mb-4"
|
||||
/>
|
||||
<h3 class="mb-4">
|
||||
{{ t('features.migration.dialog.migrating') }}
|
||||
</h3>
|
||||
<p class="mb-4">
|
||||
{{ t('features.migration.dialog.migratingSubtitle') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="console-container">
|
||||
<ConsoleDisplayer
|
||||
ref="consoleDisplayer"
|
||||
:show-level-btns="false"
|
||||
style="height: 300px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="loading" class="text-center py-8">
|
||||
<v-progress-circular indeterminate color="primary" class="mb-4"></v-progress-circular>
|
||||
<p>{{ t('features.migration.dialog.loading') }}</p>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="loading"
|
||||
class="text-center py-8"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
class="mb-4"
|
||||
/>
|
||||
<p>{{ t('features.migration.dialog.loading') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="text-center py-4">
|
||||
<v-alert type="error" variant="tonal" class="mb-4">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-alert</v-icon>
|
||||
</template>
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
<v-btn color="primary" @click="loadPlatforms">
|
||||
{{ t('features.migration.dialog.retry') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="error"
|
||||
class="text-center py-4"
|
||||
>
|
||||
<v-alert
|
||||
type="error"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-alert</v-icon>
|
||||
</template>
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="loadPlatforms"
|
||||
>
|
||||
{{ t('features.migration.dialog.retry') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div v-if="platformGroups.length === 0" class="text-center py-4">
|
||||
<v-alert type="info" variant="tonal">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-information</v-icon>
|
||||
</template>
|
||||
{{ t('features.migration.dialog.noPlatforms') }}
|
||||
</v-alert>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div
|
||||
v-if="platformGroups.length === 0"
|
||||
class="text-center py-4"
|
||||
>
|
||||
<v-alert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-information</v-icon>
|
||||
</template>
|
||||
{{ t('features.migration.dialog.noPlatforms') }}
|
||||
</v-alert>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div v-for="group in platformGroups" :key="group.type" class="mb-6">
|
||||
<v-card variant="outlined" v-if="group.platforms.length > 1">
|
||||
<v-card-subtitle class="py-2">
|
||||
{{ group.type }}
|
||||
</v-card-subtitle>
|
||||
<div v-else>
|
||||
<div
|
||||
v-for="group in platformGroups"
|
||||
:key="group.type"
|
||||
class="mb-6"
|
||||
>
|
||||
<v-card
|
||||
v-if="group.platforms.length > 1"
|
||||
variant="outlined"
|
||||
>
|
||||
<v-card-subtitle class="py-2">
|
||||
{{ group.type }}
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-divider></v-divider>
|
||||
<v-divider />
|
||||
|
||||
<v-card-text style="padding: 16px;">
|
||||
<small>请选择该平台类型下您主要使用的平台适配器。</small>
|
||||
<v-radio-group v-model="selectedPlatforms[group.type]" :key="group.type"
|
||||
hide-details>
|
||||
<v-radio v-for="platform in group.platforms" :key="platform.id"
|
||||
:value="platform.id" :label="getPlatformLabel(platform)" color="primary"
|
||||
class="mb-1"></v-radio>
|
||||
</v-radio-group>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-text style="padding: 16px;">
|
||||
<small>请选择该平台类型下您主要使用的平台适配器。</small>
|
||||
<v-radio-group
|
||||
:key="group.type"
|
||||
v-model="selectedPlatforms[group.type]"
|
||||
hide-details
|
||||
>
|
||||
<v-radio
|
||||
v-for="platform in group.platforms"
|
||||
:key="platform.id"
|
||||
:value="platform.id"
|
||||
:label="getPlatformLabel(platform)"
|
||||
color="primary"
|
||||
class="mb-1"
|
||||
/>
|
||||
</v-radio-group>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="px-6 py-4">
|
||||
<v-spacer></v-spacer>
|
||||
<template v-if="migrationCompleted">
|
||||
<v-btn color="grey" variant="text" @click="handleClose">
|
||||
{{ t('core.common.close') }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" variant="elevated" @click="restartAstrBot">
|
||||
{{ t('features.migration.dialog.restartNow') }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-btn color="grey" variant="text" @click="handleCancel" :disabled="migrating">
|
||||
{{ t('core.common.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" variant="elevated" @click="handleMigration" :disabled="!canMigrate || migrating"
|
||||
:loading="migrating">
|
||||
{{ t('features.migration.dialog.startMigration') }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-card-actions class="px-6 py-4">
|
||||
<v-spacer />
|
||||
<template v-if="migrationCompleted">
|
||||
<v-btn
|
||||
color="grey"
|
||||
variant="text"
|
||||
@click="handleClose"
|
||||
>
|
||||
{{ t('core.common.close') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
@click="restartAstrBot"
|
||||
>
|
||||
{{ t('features.migration.dialog.restartNow') }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-btn
|
||||
color="grey"
|
||||
variant="text"
|
||||
:disabled="migrating"
|
||||
@click="handleCancel"
|
||||
>
|
||||
{{ t('core.common.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
:disabled="!canMigrate || migrating"
|
||||
:loading="migrating"
|
||||
@click="handleMigration"
|
||||
>
|
||||
{{ t('features.migration.dialog.startMigration') }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<WaitingForRestart ref="wfr"></WaitingForRestart>
|
||||
<WaitingForRestart ref="wfr" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -1,35 +1,74 @@
|
||||
<template>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div>
|
||||
<span v-if="!modelValue || Object.keys(modelValue).length === 0" style="color: rgb(var(--v-theme-primaryText));">
|
||||
<span
|
||||
v-if="!modelValue || Object.keys(modelValue).length === 0"
|
||||
style="color: rgb(var(--v-theme-primaryText));"
|
||||
>
|
||||
{{ t('core.common.objectEditor.noItems') }}
|
||||
</span>
|
||||
<div v-else class="d-flex flex-wrap ga-2">
|
||||
<v-chip v-for="key in displayKeys" :key="key" size="x-small" label color="primary">
|
||||
<div
|
||||
v-else
|
||||
class="d-flex flex-wrap ga-2"
|
||||
>
|
||||
<v-chip
|
||||
v-for="key in displayKeys"
|
||||
:key="key"
|
||||
size="x-small"
|
||||
label
|
||||
color="primary"
|
||||
>
|
||||
{{ key.length > 20 ? key.slice(0, 20) + '...' : key }}
|
||||
</v-chip>
|
||||
<v-chip v-if="Object.keys(modelValue).length > maxDisplayItems" size="x-small" label color="grey-lighten-1">
|
||||
<v-chip
|
||||
v-if="Object.keys(modelValue).length > maxDisplayItems"
|
||||
size="x-small"
|
||||
label
|
||||
color="grey-lighten-1"
|
||||
>
|
||||
+{{ Object.keys(modelValue).length - maxDisplayItems }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
|
||||
<v-btn
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
@click="openDialog"
|
||||
>
|
||||
{{ resolveButtonText }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- Key-Value Management Dialog -->
|
||||
<v-dialog v-model="dialog" max-width="600px">
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
max-width="600px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
|
||||
<v-card-title
|
||||
class="text-h3 py-4"
|
||||
style="font-weight: normal;"
|
||||
>
|
||||
{{ resolveDialogTitle }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-4" style="max-height: 400px; overflow-y: auto;">
|
||||
<v-card-text
|
||||
class="pa-4"
|
||||
style="max-height: 400px; overflow-y: auto;"
|
||||
>
|
||||
<!-- Regular key-value pairs (non-template) -->
|
||||
<div v-if="nonTemplatePairs.length > 0">
|
||||
<div v-for="(pair, index) in nonTemplatePairs" :key="index" class="key-value-pair">
|
||||
<v-row no-gutters align="center" class="mb-2">
|
||||
<div
|
||||
v-for="(pair, index) in nonTemplatePairs"
|
||||
:key="index"
|
||||
class="key-value-pair"
|
||||
>
|
||||
<v-row
|
||||
no-gutters
|
||||
align="center"
|
||||
class="mb-2"
|
||||
>
|
||||
<v-col cols="4">
|
||||
<v-text-field
|
||||
v-model="pair.key"
|
||||
@@ -38,9 +77,12 @@
|
||||
hide-details
|
||||
:placeholder="t('core.common.objectEditor.placeholders.keyName')"
|
||||
@blur="updateKey(index, pair.key)"
|
||||
></v-text-field>
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="7" class="pl-2 d-flex align-center justify-end">
|
||||
<v-col
|
||||
cols="7"
|
||||
class="pl-2 d-flex align-center justify-end"
|
||||
>
|
||||
<v-text-field
|
||||
v-if="pair.type === 'string'"
|
||||
v-model="pair.value"
|
||||
@@ -48,12 +90,14 @@
|
||||
variant="outlined"
|
||||
hide-details
|
||||
:placeholder="t('core.common.objectEditor.placeholders.stringValue')"
|
||||
></v-text-field>
|
||||
<div v-else-if="pair.type === 'number' || pair.type === 'float' || pair.type === 'int'" class="d-flex align-center gap-2 flex-grow-1">
|
||||
/>
|
||||
<div
|
||||
v-else-if="pair.type === 'number' || pair.type === 'float' || pair.type === 'int'"
|
||||
class="d-flex align-center gap-2 flex-grow-1"
|
||||
>
|
||||
<v-slider
|
||||
v-if="pair.slider"
|
||||
:model-value="Number(pair.value) || 0"
|
||||
@update:model-value="pair.value = $event"
|
||||
:min="pair.slider.min"
|
||||
:max="pair.slider.max"
|
||||
:step="pair.slider.step"
|
||||
@@ -61,7 +105,8 @@
|
||||
density="compact"
|
||||
hide-details
|
||||
class="flex-grow-1"
|
||||
></v-slider>
|
||||
@update:model-value="pair.value = $event"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model.number="pair.value"
|
||||
type="number"
|
||||
@@ -70,7 +115,7 @@
|
||||
hide-details
|
||||
:placeholder="t('core.common.objectEditor.placeholders.numberValue')"
|
||||
:style="pair.slider ? 'max-width: 120px;' : ''"
|
||||
></v-text-field>
|
||||
/>
|
||||
</div>
|
||||
<v-switch
|
||||
v-else-if="pair.type === 'boolean'"
|
||||
@@ -78,7 +123,7 @@
|
||||
density="compact"
|
||||
hide-details
|
||||
color="primary"
|
||||
></v-switch>
|
||||
/>
|
||||
<v-text-field
|
||||
v-if="pair.type === 'json'"
|
||||
v-model="pair.value"
|
||||
@@ -86,11 +131,14 @@
|
||||
variant="outlined"
|
||||
hide-details="auto"
|
||||
:placeholder="t('core.common.objectEditor.placeholders.jsonValue')"
|
||||
@blur="updateJSON(index, pair.value)"
|
||||
:error-messages="pair.jsonError"
|
||||
></v-text-field>
|
||||
@blur="updateJSON(index, pair.value)"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="1" class="pl-2">
|
||||
<v-col
|
||||
cols="1"
|
||||
class="pl-2"
|
||||
>
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
@@ -106,32 +154,55 @@
|
||||
</div>
|
||||
|
||||
<!-- Template schema fields -->
|
||||
<div v-if="hasTemplateSchema" class="mt-4">
|
||||
<v-divider class="mb-3"></v-divider>
|
||||
<div class="text-caption text-grey mb-2">{{ t('core.common.objectEditor.presets') }}</div>
|
||||
<div v-for="(template, templateKey) in templateSchema" :key="templateKey" class="template-field" :class="{ 'template-field-inactive': !isTemplateKeyAdded(templateKey) }">
|
||||
<v-row no-gutters align="center" class="mb-2">
|
||||
<div
|
||||
v-if="hasTemplateSchema"
|
||||
class="mt-4"
|
||||
>
|
||||
<v-divider class="mb-3" />
|
||||
<div class="text-caption text-grey mb-2">
|
||||
{{ t('core.common.objectEditor.presets') }}
|
||||
</div>
|
||||
<div
|
||||
v-for="(template, templateKey) in templateSchema"
|
||||
:key="templateKey"
|
||||
class="template-field"
|
||||
:class="{ 'template-field-inactive': !isTemplateKeyAdded(templateKey) }"
|
||||
>
|
||||
<v-row
|
||||
no-gutters
|
||||
align="center"
|
||||
class="mb-2"
|
||||
>
|
||||
<v-col cols="4">
|
||||
<div class="d-flex flex-column">
|
||||
<span class="text-caption font-weight-medium">{{ getTemplateTitle(template, templateKey) }}</span>
|
||||
<span v-if="template.hint" class="text-caption text-grey" style="font-size: 0.7rem;">{{ translateIfKey(template.hint) }}</span>
|
||||
<span
|
||||
v-if="template.hint"
|
||||
class="text-caption text-grey"
|
||||
style="font-size: 0.7rem;"
|
||||
>{{ translateIfKey(template.hint) }}</span>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="7" class="pl-2 d-flex align-center justify-end">
|
||||
<v-col
|
||||
cols="7"
|
||||
class="pl-2 d-flex align-center justify-end"
|
||||
>
|
||||
<v-text-field
|
||||
v-if="template.type === 'string'"
|
||||
:model-value="getTemplateValue(templateKey)"
|
||||
@update:model-value="updateTemplateValue(templateKey, $event)"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
:placeholder="t('core.common.objectEditor.placeholders.stringValue')"
|
||||
></v-text-field>
|
||||
<div v-else-if="template.type === 'number' || template.type === 'float' || template.type === 'int'" class="d-flex align-center ga-4 flex-grow-1">
|
||||
@update:model-value="updateTemplateValue(templateKey, $event)"
|
||||
/>
|
||||
<div
|
||||
v-else-if="template.type === 'number' || template.type === 'float' || template.type === 'int'"
|
||||
class="d-flex align-center ga-4 flex-grow-1"
|
||||
>
|
||||
<v-slider
|
||||
v-if="template.slider"
|
||||
:model-value="Number(getTemplateValue(templateKey)) || 0"
|
||||
@update:model-value="updateTemplateValue(templateKey, $event)"
|
||||
:min="template.slider.min"
|
||||
:max="template.slider.max"
|
||||
:step="template.slider.step"
|
||||
@@ -139,28 +210,32 @@
|
||||
density="compact"
|
||||
hide-details
|
||||
class="flex-grow-1"
|
||||
></v-slider>
|
||||
@update:model-value="updateTemplateValue(templateKey, $event)"
|
||||
/>
|
||||
<v-text-field
|
||||
:model-value="getTemplateValue(templateKey)"
|
||||
@update:model-value="updateTemplateValue(templateKey, $event)"
|
||||
type="number"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
:placeholder="t('core.common.objectEditor.placeholders.numberValue')"
|
||||
:style="template.slider ? 'max-width: 120px;' : ''"
|
||||
></v-text-field>
|
||||
@update:model-value="updateTemplateValue(templateKey, $event)"
|
||||
/>
|
||||
</div>
|
||||
<v-switch
|
||||
v-else-if="template.type === 'boolean' || template.type === 'bool'"
|
||||
:model-value="getTemplateValue(templateKey)"
|
||||
@update:model-value="updateTemplateValue(templateKey, $event)"
|
||||
density="compact"
|
||||
hide-details
|
||||
color="primary"
|
||||
></v-switch>
|
||||
@update:model-value="updateTemplateValue(templateKey, $event)"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="1" class="pl-2">
|
||||
<v-col
|
||||
cols="1"
|
||||
class="pl-2"
|
||||
>
|
||||
<v-btn
|
||||
v-if="isTemplateKeyAdded(templateKey)"
|
||||
icon
|
||||
@@ -176,9 +251,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="localKeyValuePairs.length === 0 && !hasTemplateSchema" class="text-center py-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-code-json</v-icon>
|
||||
<p class="text-grey mt-4">{{ t('core.common.objectEditor.noParams') }}</p>
|
||||
<div
|
||||
v-if="localKeyValuePairs.length === 0 && !hasTemplateSchema"
|
||||
class="text-center py-8"
|
||||
>
|
||||
<v-icon
|
||||
size="64"
|
||||
color="grey-lighten-1"
|
||||
>
|
||||
mdi-code-json
|
||||
</v-icon>
|
||||
<p class="text-grey mt-4">
|
||||
{{ t('core.common.objectEditor.noParams') }}
|
||||
</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
@@ -192,7 +277,7 @@
|
||||
variant="outlined"
|
||||
hide-details
|
||||
class="flex-grow-1"
|
||||
></v-text-field>
|
||||
/>
|
||||
<v-select
|
||||
v-model="newValueType"
|
||||
:items="['string', 'number', 'boolean', 'json']"
|
||||
@@ -201,8 +286,12 @@
|
||||
variant="outlined"
|
||||
hide-details
|
||||
style="max-width: 120px;"
|
||||
></v-select>
|
||||
<v-btn @click="addKeyValuePair" variant="tonal" color="primary">
|
||||
/>
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
@click="addKeyValuePair"
|
||||
>
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
{{ t('core.common.add') }}
|
||||
</v-btn>
|
||||
@@ -210,9 +299,19 @@
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="cancelDialog">{{ t('core.common.cancel') }}</v-btn>
|
||||
<v-btn color="primary" @click="confirmDialog">{{ t('core.common.confirm') }}</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="cancelDialog"
|
||||
>
|
||||
{{ t('core.common.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="confirmDialog"
|
||||
>
|
||||
{{ t('core.common.confirm') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
@@ -1,325 +1,584 @@
|
||||
<template>
|
||||
<v-dialog v-model="showDialog" :max-width="$vuetify.display.smAndDown ? undefined : '1200px'" scrollable>
|
||||
<v-card class="persona-form-card" :class="{ 'persona-form-card-mobile': $vuetify.display.smAndDown }">
|
||||
<v-card-title class="persona-form-title text-h2 px-6 pt-6 pl-6">
|
||||
{{ editingPersona ? tm('dialog.edit.title') : tm('dialog.create.title') }}
|
||||
</v-card-title>
|
||||
<v-dialog
|
||||
v-model="showDialog"
|
||||
:max-width="$vuetify.display.smAndDown ? undefined : '1200px'"
|
||||
scrollable
|
||||
>
|
||||
<v-card
|
||||
class="persona-form-card"
|
||||
:class="{ 'persona-form-card-mobile': $vuetify.display.smAndDown }"
|
||||
>
|
||||
<v-card-title class="persona-form-title text-h2 px-6 pt-6 pl-6">
|
||||
{{ editingPersona ? tm('dialog.edit.title') : tm('dialog.create.title') }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="persona-form-content">
|
||||
<!-- 创建位置提示 -->
|
||||
<v-alert v-if="!editingPersona" type="info" variant="tonal" density="compact" class="mb-4"
|
||||
icon="mdi-folder-outline">
|
||||
{{ tm('form.createInFolder', { folder: folderDisplayName }) }}
|
||||
</v-alert>
|
||||
<v-card-text class="persona-form-content">
|
||||
<!-- 创建位置提示 -->
|
||||
<v-alert
|
||||
v-if="!editingPersona"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
class="mb-4"
|
||||
icon="mdi-folder-outline"
|
||||
>
|
||||
{{ tm('form.createInFolder', { folder: folderDisplayName }) }}
|
||||
</v-alert>
|
||||
|
||||
<v-form ref="personaForm" v-model="formValid">
|
||||
<v-row class="persona-form-layout">
|
||||
<v-col cols="12" md="6" class="persona-basic-col">
|
||||
<v-text-field v-model="personaForm.persona_id" :label="tm('form.personaId')"
|
||||
:rules="personaIdRules" :disabled="editingPersona" variant="outlined"
|
||||
density="comfortable" class="mb-4" />
|
||||
<v-form
|
||||
ref="personaForm"
|
||||
v-model="formValid"
|
||||
>
|
||||
<v-row class="persona-form-layout">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
class="persona-basic-col"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="personaForm.persona_id"
|
||||
:label="tm('form.personaId')"
|
||||
:rules="personaIdRules"
|
||||
:disabled="editingPersona"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<v-textarea v-model="personaForm.system_prompt" :label="tm('form.systemPrompt')"
|
||||
:rules="systemPromptRules" variant="outlined" rows="16" class="mb-4" />
|
||||
<v-textarea
|
||||
v-model="personaForm.system_prompt"
|
||||
:label="tm('form.systemPrompt')"
|
||||
:rules="systemPromptRules"
|
||||
variant="outlined"
|
||||
rows="16"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<v-textarea
|
||||
v-model="personaForm.custom_error_message"
|
||||
:label="tm('form.customErrorMessage')"
|
||||
:hint="tm('form.customErrorMessageHelp')"
|
||||
variant="outlined"
|
||||
rows="4"
|
||||
persistent-hint
|
||||
clearable
|
||||
class="mb-4"
|
||||
/>
|
||||
</v-col>
|
||||
<v-textarea
|
||||
v-model="personaForm.custom_error_message"
|
||||
:label="tm('form.customErrorMessage')"
|
||||
:hint="tm('form.customErrorMessageHelp')"
|
||||
variant="outlined"
|
||||
rows="4"
|
||||
persistent-hint
|
||||
clearable
|
||||
class="mb-4"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6" class="persona-panels-col">
|
||||
<v-expansion-panels v-model="expandedPanels" multiple>
|
||||
<!-- 工具选择面板 -->
|
||||
<v-expansion-panel value="tools">
|
||||
<v-expansion-panel-title>
|
||||
<v-icon class="mr-2">mdi-tools</v-icon>
|
||||
{{ tm('form.tools') }}
|
||||
<v-chip v-if="Array.isArray(personaForm.tools) && personaForm.tools.length > 0"
|
||||
size="small" color="primary" variant="tonal" class="ml-2">
|
||||
{{ personaForm.tools.length }}
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
class="persona-panels-col"
|
||||
>
|
||||
<v-expansion-panels
|
||||
v-model="expandedPanels"
|
||||
multiple
|
||||
>
|
||||
<!-- 工具选择面板 -->
|
||||
<v-expansion-panel value="tools">
|
||||
<v-expansion-panel-title>
|
||||
<v-icon class="mr-2">
|
||||
mdi-tools
|
||||
</v-icon>
|
||||
{{ tm('form.tools') }}
|
||||
<v-chip
|
||||
v-if="Array.isArray(personaForm.tools) && personaForm.tools.length > 0"
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
class="ml-2"
|
||||
>
|
||||
{{ personaForm.tools.length }}
|
||||
</v-chip>
|
||||
</v-expansion-panel-title>
|
||||
|
||||
<v-expansion-panel-text>
|
||||
<div class="mb-3">
|
||||
<p class="text-body-2 text-medium-emphasis">
|
||||
{{ tm('form.toolsHelp') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<v-radio-group
|
||||
v-model="toolSelectValue"
|
||||
class="mt-2"
|
||||
hide-details="true"
|
||||
>
|
||||
<v-radio
|
||||
label="默认使用全部函数工具"
|
||||
value="0"
|
||||
/>
|
||||
<v-radio
|
||||
label="选择指定函数工具"
|
||||
value="1"
|
||||
/>
|
||||
</v-radio-group>
|
||||
|
||||
<div
|
||||
v-if="toolSelectValue === '1'"
|
||||
class="mt-3 selected-config-area"
|
||||
>
|
||||
<!-- 工具搜索 -->
|
||||
<v-text-field
|
||||
v-model="toolSearch"
|
||||
:label="tm('form.searchTools')"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
clearable
|
||||
class="mb-3"
|
||||
/>
|
||||
|
||||
|
||||
<!-- MCP 服务器 -->
|
||||
<div
|
||||
v-if="mcpServers.length > 0"
|
||||
class="mb-4"
|
||||
>
|
||||
<h4 class="text-subtitle-2 mb-2">
|
||||
{{ tm('form.mcpServersQuickSelect') }}
|
||||
</h4>
|
||||
<div class="d-flex flex-wrap ga-2">
|
||||
<v-chip
|
||||
v-for="server in mcpServers"
|
||||
:key="server.name"
|
||||
:color="isServerSelected(server) ? 'primary' : 'default'"
|
||||
:variant="isServerSelected(server) ? 'flat' : 'outlined'"
|
||||
size="small"
|
||||
clickable
|
||||
:disabled="!server.tools || server.tools.length === 0"
|
||||
@click="toggleMcpServer(server)"
|
||||
>
|
||||
<v-icon
|
||||
start
|
||||
size="small"
|
||||
>
|
||||
mdi-server
|
||||
</v-icon>
|
||||
{{ server.name }}
|
||||
<v-chip-text
|
||||
v-if="server.tools"
|
||||
class="ml-1"
|
||||
>
|
||||
({{ server.tools.length }})
|
||||
</v-chip-text>
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工具选择列表 -->
|
||||
<div
|
||||
v-if="filteredTools.length > 0"
|
||||
class="tools-selection"
|
||||
>
|
||||
<v-virtual-scroll
|
||||
:items="filteredTools"
|
||||
height="300"
|
||||
item-height="72"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<v-list-item
|
||||
:key="item.name"
|
||||
density="comfortable"
|
||||
@click="toggleTool(item.name)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-checkbox-btn
|
||||
:model-value="isToolSelected(item.name)"
|
||||
@click.stop="toggleTool(item.name)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<v-list-item-title>
|
||||
{{ item.name }}
|
||||
|
||||
<v-chip
|
||||
v-if="item.origin"
|
||||
size="x-small"
|
||||
color="info"
|
||||
class="mr-2"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ item.origin }}
|
||||
</v-chip>
|
||||
</v-expansion-panel-title>
|
||||
|
||||
<v-expansion-panel-text>
|
||||
<div class="mb-3">
|
||||
<p class="text-body-2 text-medium-emphasis">
|
||||
{{ tm('form.toolsHelp') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<v-radio-group class="mt-2" v-model="toolSelectValue" hide-details="true">
|
||||
<v-radio label="默认使用全部函数工具" value="0"></v-radio>
|
||||
<v-radio label="选择指定函数工具" value="1">
|
||||
</v-radio>
|
||||
</v-radio-group>
|
||||
|
||||
<div v-if="toolSelectValue === '1'" class="mt-3 selected-config-area">
|
||||
|
||||
<!-- 工具搜索 -->
|
||||
<v-text-field v-model="toolSearch" :label="tm('form.searchTools')"
|
||||
prepend-inner-icon="mdi-magnify" variant="outlined" density="compact"
|
||||
hide-details clearable class="mb-3" />
|
||||
|
||||
|
||||
<!-- MCP 服务器 -->
|
||||
<div v-if="mcpServers.length > 0" class="mb-4">
|
||||
<h4 class="text-subtitle-2 mb-2">{{ tm('form.mcpServersQuickSelect') }}</h4>
|
||||
<div class="d-flex flex-wrap ga-2">
|
||||
<v-chip v-for="server in mcpServers" :key="server.name"
|
||||
:color="isServerSelected(server) ? 'primary' : 'default'"
|
||||
:variant="isServerSelected(server) ? 'flat' : 'outlined'" size="small"
|
||||
clickable @click="toggleMcpServer(server)"
|
||||
:disabled="!server.tools || server.tools.length === 0">
|
||||
<v-icon start size="small">mdi-server</v-icon>
|
||||
{{ server.name }}
|
||||
<v-chip-text v-if="server.tools" class="ml-1">
|
||||
({{ server.tools.length }})
|
||||
</v-chip-text>
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工具选择列表 -->
|
||||
<div v-if="filteredTools.length > 0" class="tools-selection">
|
||||
<v-virtual-scroll :items="filteredTools" height="300" item-height="72">
|
||||
<template v-slot:default="{ item }">
|
||||
<v-list-item :key="item.name" density="comfortable"
|
||||
@click="toggleTool(item.name)">
|
||||
<template v-slot:prepend>
|
||||
<v-checkbox-btn :model-value="isToolSelected(item.name)"
|
||||
@click.stop="toggleTool(item.name)" />
|
||||
</template>
|
||||
|
||||
<v-list-item-title>
|
||||
{{ item.name }}
|
||||
|
||||
<v-chip v-if="item.origin" size="x-small" color="info" class="mr-2"
|
||||
variant="tonal">
|
||||
{{ item.origin }}
|
||||
</v-chip>
|
||||
<v-chip v-if="item.origin_name" size="x-small" color="info"
|
||||
variant="outlined">
|
||||
{{ item.origin_name }}
|
||||
</v-chip>
|
||||
|
||||
</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle v-if="item.description">
|
||||
{{ truncateText(item.description, 100) }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-virtual-scroll>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!loadingTools && availableTools.length === 0"
|
||||
class="text-center pa-4">
|
||||
<v-icon size="48" color="grey-lighten-2" class="mb-2">mdi-tools</v-icon>
|
||||
<p class="text-body-2 text-medium-emphasis">{{ tm('form.noToolsAvailable')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!loadingTools && filteredTools.length === 0"
|
||||
class="text-center pa-4">
|
||||
<v-icon size="48" color="grey-lighten-2" class="mb-2">mdi-magnify</v-icon>
|
||||
<p class="text-body-2 text-medium-emphasis">{{ tm('form.noToolsFound') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loadingTools" class="text-center pa-4">
|
||||
<v-progress-circular indeterminate color="primary" />
|
||||
<p class="text-body-2 text-medium-emphasis mt-2">{{ tm('form.loadingTools')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 已选择的工具 -->
|
||||
<div class="mt-4">
|
||||
<h4 class="text-subtitle-2 mb-2">
|
||||
{{ tm('form.selectedTools') }}
|
||||
<span v-if="personaForm.tools === null" class="text-success">
|
||||
({{ tm('form.allSelected') }})
|
||||
</span>
|
||||
<span v-else-if="Array.isArray(personaForm.tools)">
|
||||
({{ personaForm.tools.length }})
|
||||
</span>
|
||||
</h4>
|
||||
<div v-if="Array.isArray(personaForm.tools) && personaForm.tools.length > 0"
|
||||
class="d-flex flex-wrap ga-1" style="max-height: 100px; overflow-y: auto;">
|
||||
<v-chip v-for="toolName in personaForm.tools" :key="toolName" size="small"
|
||||
color="primary" variant="tonal" closable
|
||||
@click:close="removeTool(toolName)">
|
||||
{{ toolName }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<div v-else class="text-body-2 text-medium-emphasis">
|
||||
{{ tm('form.noToolsSelected') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
|
||||
<!-- Skills 选择面板 -->
|
||||
<v-expansion-panel value="skills">
|
||||
<v-expansion-panel-title>
|
||||
<v-icon class="mr-2">mdi-lightning-bolt</v-icon>
|
||||
{{ tm('form.skills') }}
|
||||
<v-chip v-if="Array.isArray(personaForm.skills) && personaForm.skills.length > 0"
|
||||
size="small" color="primary" variant="tonal" class="ml-2">
|
||||
{{ personaForm.skills.length }}
|
||||
<v-chip
|
||||
v-if="item.origin_name"
|
||||
size="x-small"
|
||||
color="info"
|
||||
variant="outlined"
|
||||
>
|
||||
{{ item.origin_name }}
|
||||
</v-chip>
|
||||
</v-expansion-panel-title>
|
||||
</v-list-item-title>
|
||||
|
||||
<v-expansion-panel-text>
|
||||
<div class="mb-3">
|
||||
<p class="text-body-2 text-medium-emphasis">
|
||||
{{ tm('form.skillsHelp') }}
|
||||
</p>
|
||||
</div>
|
||||
<v-list-item-subtitle v-if="item.description">
|
||||
{{ truncateText(item.description, 100) }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-virtual-scroll>
|
||||
</div>
|
||||
|
||||
<v-radio-group class="mt-2" v-model="skillSelectValue" hide-details="true">
|
||||
<v-radio :label="tm('form.skillsAllAvailable')" value="0"></v-radio>
|
||||
<v-radio :label="tm('form.skillsSelectSpecific')" value="1"></v-radio>
|
||||
</v-radio-group>
|
||||
<div
|
||||
v-else-if="!loadingTools && availableTools.length === 0"
|
||||
class="text-center pa-4"
|
||||
>
|
||||
<v-icon
|
||||
size="48"
|
||||
color="grey-lighten-2"
|
||||
class="mb-2"
|
||||
>
|
||||
mdi-tools
|
||||
</v-icon>
|
||||
<p class="text-body-2 text-medium-emphasis">
|
||||
{{ tm('form.noToolsAvailable')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="skillSelectValue === '1'" class="mt-3 selected-config-area">
|
||||
<v-text-field v-model="skillSearch" :label="tm('form.searchSkills')"
|
||||
prepend-inner-icon="mdi-magnify" variant="outlined" density="compact"
|
||||
hide-details clearable class="mb-3" />
|
||||
<div
|
||||
v-else-if="!loadingTools && filteredTools.length === 0"
|
||||
class="text-center pa-4"
|
||||
>
|
||||
<v-icon
|
||||
size="48"
|
||||
color="grey-lighten-2"
|
||||
class="mb-2"
|
||||
>
|
||||
mdi-magnify
|
||||
</v-icon>
|
||||
<p class="text-body-2 text-medium-emphasis">
|
||||
{{ tm('form.noToolsFound') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="filteredSkills.length > 0" class="skills-selection">
|
||||
<v-virtual-scroll :items="filteredSkills" height="240" item-height="48">
|
||||
<template v-slot:default="{ item }">
|
||||
<v-list-item :key="item.name" density="comfortable"
|
||||
@click="toggleSkill(item.name)">
|
||||
<template v-slot:prepend>
|
||||
<v-checkbox-btn :model-value="isSkillSelected(item.name)"
|
||||
@click.stop="toggleSkill(item.name)" />
|
||||
</template>
|
||||
<v-list-item-title>
|
||||
{{ item.name }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="item.description">
|
||||
{{ truncateText(item.description, 100) }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-virtual-scroll>
|
||||
</div>
|
||||
<!-- 加载状态 -->
|
||||
<div
|
||||
v-if="loadingTools"
|
||||
class="text-center pa-4"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
<p class="text-body-2 text-medium-emphasis mt-2">
|
||||
{{ tm('form.loadingTools')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!loadingSkills && availableSkills.length === 0"
|
||||
class="text-center pa-4">
|
||||
<v-icon size="48" color="grey-lighten-2"
|
||||
class="mb-2">mdi-lightning-bolt</v-icon>
|
||||
<p class="text-body-2 text-medium-emphasis">{{ tm('form.noSkillsAvailable') }}
|
||||
</p>
|
||||
</div>
|
||||
<!-- 已选择的工具 -->
|
||||
<div class="mt-4">
|
||||
<h4 class="text-subtitle-2 mb-2">
|
||||
{{ tm('form.selectedTools') }}
|
||||
<span
|
||||
v-if="personaForm.tools === null"
|
||||
class="text-success"
|
||||
>
|
||||
({{ tm('form.allSelected') }})
|
||||
</span>
|
||||
<span v-else-if="Array.isArray(personaForm.tools)">
|
||||
({{ personaForm.tools.length }})
|
||||
</span>
|
||||
</h4>
|
||||
<div
|
||||
v-if="Array.isArray(personaForm.tools) && personaForm.tools.length > 0"
|
||||
class="d-flex flex-wrap ga-1"
|
||||
style="max-height: 100px; overflow-y: auto;"
|
||||
>
|
||||
<v-chip
|
||||
v-for="toolName in personaForm.tools"
|
||||
:key="toolName"
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
closable
|
||||
@click:close="removeTool(toolName)"
|
||||
>
|
||||
{{ toolName }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="text-body-2 text-medium-emphasis"
|
||||
>
|
||||
{{ tm('form.noToolsSelected') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
|
||||
<div v-else-if="!loadingSkills && filteredSkills.length === 0"
|
||||
class="text-center pa-4">
|
||||
<v-icon size="48" color="grey-lighten-2" class="mb-2">mdi-magnify</v-icon>
|
||||
<p class="text-body-2 text-medium-emphasis">{{ tm('form.noSkillsFound') }}
|
||||
</p>
|
||||
</div>
|
||||
<!-- Skills 选择面板 -->
|
||||
<v-expansion-panel value="skills">
|
||||
<v-expansion-panel-title>
|
||||
<v-icon class="mr-2">
|
||||
mdi-lightning-bolt
|
||||
</v-icon>
|
||||
{{ tm('form.skills') }}
|
||||
<v-chip
|
||||
v-if="Array.isArray(personaForm.skills) && personaForm.skills.length > 0"
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
class="ml-2"
|
||||
>
|
||||
{{ personaForm.skills.length }}
|
||||
</v-chip>
|
||||
</v-expansion-panel-title>
|
||||
|
||||
<div v-if="loadingSkills" class="text-center pa-4">
|
||||
<v-progress-circular indeterminate color="primary" />
|
||||
<p class="text-body-2 text-medium-emphasis mt-2">{{ tm('form.loadingSkills') }}
|
||||
</p>
|
||||
</div>
|
||||
<v-expansion-panel-text>
|
||||
<div class="mb-3">
|
||||
<p class="text-body-2 text-medium-emphasis">
|
||||
{{ tm('form.skillsHelp') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h4 class="text-subtitle-2 mb-2">
|
||||
{{ tm('form.selectedSkills') }}
|
||||
<span v-if="personaForm.skills === null" class="text-success">
|
||||
({{ tm('form.allSelected') }})
|
||||
</span>
|
||||
<span v-else-if="Array.isArray(personaForm.skills)">
|
||||
({{ personaForm.skills.length }})
|
||||
</span>
|
||||
</h4>
|
||||
<div v-if="Array.isArray(personaForm.skills) && personaForm.skills.length > 0"
|
||||
class="d-flex flex-wrap ga-1" style="max-height: 100px; overflow-y: auto;">
|
||||
<v-chip v-for="skillName in personaForm.skills" :key="skillName"
|
||||
size="small" color="primary" variant="tonal" closable
|
||||
@click:close="removeSkill(skillName)">
|
||||
{{ skillName }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<div v-else class="text-body-2 text-medium-emphasis">
|
||||
{{ tm('form.noSkillsSelected') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
<v-radio-group
|
||||
v-model="skillSelectValue"
|
||||
class="mt-2"
|
||||
hide-details="true"
|
||||
>
|
||||
<v-radio
|
||||
:label="tm('form.skillsAllAvailable')"
|
||||
value="0"
|
||||
/>
|
||||
<v-radio
|
||||
:label="tm('form.skillsSelectSpecific')"
|
||||
value="1"
|
||||
/>
|
||||
</v-radio-group>
|
||||
|
||||
<!-- 预设对话面板 -->
|
||||
<v-expansion-panel value="dialogs">
|
||||
<v-expansion-panel-title>
|
||||
<v-icon class="mr-2">mdi-chat</v-icon>
|
||||
{{ tm('form.presetDialogs') }}
|
||||
<v-chip v-if="personaForm.begin_dialogs.length > 0" size="small" color="primary"
|
||||
variant="tonal" class="ml-2">
|
||||
{{ personaForm.begin_dialogs.length / 2 }}
|
||||
</v-chip>
|
||||
</v-expansion-panel-title>
|
||||
<div
|
||||
v-if="skillSelectValue === '1'"
|
||||
class="mt-3 selected-config-area"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="skillSearch"
|
||||
:label="tm('form.searchSkills')"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
clearable
|
||||
class="mb-3"
|
||||
/>
|
||||
|
||||
<v-expansion-panel-text>
|
||||
<div class="mb-3">
|
||||
<p class="text-body-2 text-medium-emphasis">
|
||||
{{ tm('form.presetDialogsHelp') }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="filteredSkills.length > 0"
|
||||
class="skills-selection"
|
||||
>
|
||||
<v-virtual-scroll
|
||||
:items="filteredSkills"
|
||||
height="240"
|
||||
item-height="48"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<v-list-item
|
||||
:key="item.name"
|
||||
density="comfortable"
|
||||
@click="toggleSkill(item.name)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-checkbox-btn
|
||||
:model-value="isSkillSelected(item.name)"
|
||||
@click.stop="toggleSkill(item.name)"
|
||||
/>
|
||||
</template>
|
||||
<v-list-item-title>
|
||||
{{ item.name }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="item.description">
|
||||
{{ truncateText(item.description, 100) }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-virtual-scroll>
|
||||
</div>
|
||||
|
||||
<div v-for="(dialog, index) in personaForm.begin_dialogs" :key="index" class="mb-3">
|
||||
<v-textarea v-model="personaForm.begin_dialogs[index]"
|
||||
:label="index % 2 === 0 ? tm('form.userMessage') : tm('form.assistantMessage')"
|
||||
:rules="getDialogRules(index)" variant="outlined" rows="2"
|
||||
density="comfortable">
|
||||
<template v-slot:append>
|
||||
<v-btn icon="mdi-delete" variant="text" size="small" color="error"
|
||||
@click="removeDialog(index)" />
|
||||
</template>
|
||||
</v-textarea>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="!loadingSkills && availableSkills.length === 0"
|
||||
class="text-center pa-4"
|
||||
>
|
||||
<v-icon
|
||||
size="48"
|
||||
color="grey-lighten-2"
|
||||
class="mb-2"
|
||||
>
|
||||
mdi-lightning-bolt
|
||||
</v-icon>
|
||||
<p class="text-body-2 text-medium-emphasis">
|
||||
{{ tm('form.noSkillsAvailable') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<v-btn variant="outlined" prepend-icon="mdi-plus" @click="addDialogPair" block>
|
||||
{{ tm('buttons.addDialogPair') }}
|
||||
</v-btn>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<div
|
||||
v-else-if="!loadingSkills && filteredSkills.length === 0"
|
||||
class="text-center pa-4"
|
||||
>
|
||||
<v-icon
|
||||
size="48"
|
||||
color="grey-lighten-2"
|
||||
class="mb-2"
|
||||
>
|
||||
mdi-magnify
|
||||
</v-icon>
|
||||
<p class="text-body-2 text-medium-emphasis">
|
||||
{{ tm('form.noSkillsFound') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<v-card-actions class="persona-form-actions">
|
||||
<v-btn v-if="editingPersona" color="error" variant="text" @click="deletePersona">
|
||||
{{ tm('buttons.delete') }}
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn color="grey" variant="text" @click="closeDialog">
|
||||
{{ tm('buttons.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" variant="flat" @click="savePersona" :loading="saving" :disabled="!formValid">
|
||||
{{ tm('buttons.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<div
|
||||
v-if="loadingSkills"
|
||||
class="text-center pa-4"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
<p class="text-body-2 text-medium-emphasis mt-2">
|
||||
{{ tm('form.loadingSkills') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h4 class="text-subtitle-2 mb-2">
|
||||
{{ tm('form.selectedSkills') }}
|
||||
<span
|
||||
v-if="personaForm.skills === null"
|
||||
class="text-success"
|
||||
>
|
||||
({{ tm('form.allSelected') }})
|
||||
</span>
|
||||
<span v-else-if="Array.isArray(personaForm.skills)">
|
||||
({{ personaForm.skills.length }})
|
||||
</span>
|
||||
</h4>
|
||||
<div
|
||||
v-if="Array.isArray(personaForm.skills) && personaForm.skills.length > 0"
|
||||
class="d-flex flex-wrap ga-1"
|
||||
style="max-height: 100px; overflow-y: auto;"
|
||||
>
|
||||
<v-chip
|
||||
v-for="skillName in personaForm.skills"
|
||||
:key="skillName"
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
closable
|
||||
@click:close="removeSkill(skillName)"
|
||||
>
|
||||
{{ skillName }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="text-body-2 text-medium-emphasis"
|
||||
>
|
||||
{{ tm('form.noSkillsSelected') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
|
||||
<!-- 预设对话面板 -->
|
||||
<v-expansion-panel value="dialogs">
|
||||
<v-expansion-panel-title>
|
||||
<v-icon class="mr-2">
|
||||
mdi-chat
|
||||
</v-icon>
|
||||
{{ tm('form.presetDialogs') }}
|
||||
<v-chip
|
||||
v-if="personaForm.begin_dialogs.length > 0"
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
class="ml-2"
|
||||
>
|
||||
{{ personaForm.begin_dialogs.length / 2 }}
|
||||
</v-chip>
|
||||
</v-expansion-panel-title>
|
||||
|
||||
<v-expansion-panel-text>
|
||||
<div class="mb-3">
|
||||
<p class="text-body-2 text-medium-emphasis">
|
||||
{{ tm('form.presetDialogsHelp') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(dialog, index) in personaForm.begin_dialogs"
|
||||
:key="index"
|
||||
class="mb-3"
|
||||
>
|
||||
<v-textarea
|
||||
v-model="personaForm.begin_dialogs[index]"
|
||||
:label="index % 2 === 0 ? tm('form.userMessage') : tm('form.assistantMessage')"
|
||||
:rules="getDialogRules(index)"
|
||||
variant="outlined"
|
||||
rows="2"
|
||||
density="comfortable"
|
||||
>
|
||||
<template #append>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
variant="text"
|
||||
size="small"
|
||||
color="error"
|
||||
@click="removeDialog(index)"
|
||||
/>
|
||||
</template>
|
||||
</v-textarea>
|
||||
</div>
|
||||
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-plus"
|
||||
block
|
||||
@click="addDialogPair"
|
||||
>
|
||||
{{ tm('buttons.addDialogPair') }}
|
||||
</v-btn>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="persona-form-actions">
|
||||
<v-btn
|
||||
v-if="editingPersona"
|
||||
color="error"
|
||||
variant="text"
|
||||
@click="deletePersona"
|
||||
>
|
||||
{{ tm('buttons.delete') }}
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="grey"
|
||||
variant="text"
|
||||
@click="closeDialog"
|
||||
>
|
||||
{{ tm('buttons.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="flat"
|
||||
:loading="saving"
|
||||
:disabled="!formValid"
|
||||
@click="savePersona"
|
||||
>
|
||||
{{ tm('buttons.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -4,24 +4,46 @@
|
||||
<small>{{ tm('personaQuickPreview.title') }}</small>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="preview-loading">
|
||||
<v-progress-circular indeterminate size="18" width="2" color="primary" class="mr-2" />
|
||||
<div
|
||||
v-if="loading"
|
||||
class="preview-loading"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
size="18"
|
||||
width="2"
|
||||
color="primary"
|
||||
class="mr-2"
|
||||
/>
|
||||
<small class="text-grey">{{ tm('personaQuickPreview.loading') }}</small>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!modelValue" class="preview-empty">
|
||||
<div
|
||||
v-else-if="!modelValue"
|
||||
class="preview-empty"
|
||||
>
|
||||
<small class="text-grey">{{ tm('personaQuickPreview.noPersonaSelected') }}</small>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!personaData" class="preview-empty">
|
||||
<div
|
||||
v-else-if="!personaData"
|
||||
class="preview-empty"
|
||||
>
|
||||
<small class="text-grey">{{ tm('personaQuickPreview.personaNotFound') }}</small>
|
||||
</div>
|
||||
|
||||
<div v-else class="preview-content">
|
||||
<div class="section-title">{{ tm('personaQuickPreview.systemPromptLabel') }}</div>
|
||||
<div
|
||||
v-else
|
||||
class="preview-content"
|
||||
>
|
||||
<div class="section-title">
|
||||
{{ tm('personaQuickPreview.systemPromptLabel') }}
|
||||
</div>
|
||||
<pre class="prompt-content">{{ personaData.system_prompt || '' }}</pre>
|
||||
|
||||
<div class="section-title mt-3">{{ tm('personaQuickPreview.toolsLabel') }}</div>
|
||||
<div class="section-title mt-3">
|
||||
{{ tm('personaQuickPreview.toolsLabel') }}
|
||||
</div>
|
||||
<div class="chip-wrap tools-wrap">
|
||||
<v-chip
|
||||
v-if="personaData.tools === null"
|
||||
@@ -32,7 +54,12 @@
|
||||
>
|
||||
{{ tm('personaQuickPreview.allToolsWithCount', { count: allToolsCount }) }}
|
||||
</v-chip>
|
||||
<div v-for="tool in resolvedTools" v-else :key="tool.name" class="tool-item">
|
||||
<div
|
||||
v-for="tool in resolvedTools"
|
||||
v-else
|
||||
:key="tool.name"
|
||||
class="tool-item"
|
||||
>
|
||||
<v-chip
|
||||
size="small"
|
||||
:color="tool.active === false ? 'warning' : 'primary'"
|
||||
@@ -41,25 +68,39 @@
|
||||
>
|
||||
{{ tool.name }}
|
||||
</v-chip>
|
||||
<v-tooltip v-if="tool.active === false" location="top">
|
||||
<template v-slot:activator="{ props: tooltipProps }">
|
||||
<small class="text-warning tool-inactive" v-bind="tooltipProps">
|
||||
<v-tooltip
|
||||
v-if="tool.active === false"
|
||||
location="top"
|
||||
>
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<small
|
||||
class="text-warning tool-inactive"
|
||||
v-bind="tooltipProps"
|
||||
>
|
||||
{{ tm('personaQuickPreview.toolInactive') }}
|
||||
</small>
|
||||
</template>
|
||||
{{ tm('personaQuickPreview.toolInactiveTooltip') }}
|
||||
</v-tooltip>
|
||||
<small v-if="tool.origin || tool.origin_name" class="text-grey tool-meta">
|
||||
<small
|
||||
v-if="tool.origin || tool.origin_name"
|
||||
class="text-grey tool-meta"
|
||||
>
|
||||
<span v-if="tool.origin">{{ tm('personaQuickPreview.originLabel') }}: {{ tool.origin }}</span>
|
||||
<span v-if="tool.origin_name"> | {{ tm('personaQuickPreview.originNameLabel') }}: {{ tool.origin_name }}</span>
|
||||
</small>
|
||||
</div>
|
||||
<small v-if="personaData.tools !== null && normalizedTools.length === 0" class="text-grey">
|
||||
<small
|
||||
v-if="personaData.tools !== null && normalizedTools.length === 0"
|
||||
class="text-grey"
|
||||
>
|
||||
{{ tm('personaQuickPreview.noTools') }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="section-title mt-3">{{ tm('personaQuickPreview.skillsLabel') }}</div>
|
||||
<div class="section-title mt-3">
|
||||
{{ tm('personaQuickPreview.skillsLabel') }}
|
||||
</div>
|
||||
<div class="chip-wrap">
|
||||
<v-chip
|
||||
v-if="personaData.skills === null"
|
||||
@@ -81,7 +122,10 @@
|
||||
>
|
||||
{{ skillName }}
|
||||
</v-chip>
|
||||
<small v-if="personaData.skills !== null && normalizedSkills.length === 0" class="text-grey">
|
||||
<small
|
||||
v-if="personaData.skills !== null && normalizedSkills.length === 0"
|
||||
class="text-grey"
|
||||
>
|
||||
{{ tm('personaQuickPreview.noSkills') }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<BaseFolderItemSelector
|
||||
:model-value="modelValue"
|
||||
@update:model-value="handleUpdate"
|
||||
:folder-tree="folderTree"
|
||||
:items="currentPersonas as any"
|
||||
:tree-loading="treeLoading"
|
||||
@@ -14,6 +13,7 @@
|
||||
item-name-field="persona_id"
|
||||
item-description-field="system_prompt"
|
||||
:display-value-formatter="formatDisplayValue"
|
||||
@update:model-value="handleUpdate"
|
||||
@navigate="handleNavigate"
|
||||
@create="openCreatePersona"
|
||||
@edit="openEditPersona"
|
||||
@@ -26,7 +26,8 @@
|
||||
:current-folder-id="currentFolderId ?? undefined"
|
||||
:current-folder-name="currentFolderName ?? undefined"
|
||||
@saved="handlePersonaSaved"
|
||||
@error="handleError" />
|
||||
@error="handleError"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -45,9 +45,15 @@ const platformDetails = computed(() => {
|
||||
:style="{ cursor: 'pointer', ...chipStyle }"
|
||||
@click.stop="showMenu = !showMenu"
|
||||
>
|
||||
<div class="d-flex align-center" style="gap: 2px">
|
||||
<div
|
||||
class="d-flex align-center"
|
||||
style="gap: 2px"
|
||||
>
|
||||
<!-- 显示图标,最多 5 个 -->
|
||||
<div class="d-flex align-center mr-1" v-if="platformDetails.some(p => p.icon)">
|
||||
<div
|
||||
v-if="platformDetails.some(p => p.icon)"
|
||||
class="d-flex align-center mr-1"
|
||||
>
|
||||
<v-avatar
|
||||
v-for="(platform, index) in platformDetails.slice(0, 5)"
|
||||
:key="index"
|
||||
@@ -55,8 +61,15 @@ const platformDetails = computed(() => {
|
||||
class="platform-mini-icon"
|
||||
:style="{ marginLeft: index > 0 ? '-4px' : '0', zIndex: 10 - index }"
|
||||
>
|
||||
<v-img v-if="platform.icon" :src="platform.icon"></v-img>
|
||||
<v-icon v-else icon="mdi-circle-small" :size="size === 'x-small' ? 8 : 10"></v-icon>
|
||||
<v-img
|
||||
v-if="platform.icon"
|
||||
:src="platform.icon"
|
||||
/>
|
||||
<v-icon
|
||||
v-else
|
||||
icon="mdi-circle-small"
|
||||
:size="size === 'x-small' ? 8 : 10"
|
||||
/>
|
||||
</v-avatar>
|
||||
</div>
|
||||
|
||||
@@ -72,7 +85,7 @@ const platformDetails = computed(() => {
|
||||
:icon="showMenu ? 'mdi-chevron-up' : 'mdi-chevron-down'"
|
||||
:size="size === 'x-small' ? 14 : 16"
|
||||
class="ml-n1"
|
||||
></v-icon>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<v-menu
|
||||
@@ -83,20 +96,37 @@ const platformDetails = computed(() => {
|
||||
transition="scale-transition"
|
||||
open-on-hover
|
||||
>
|
||||
<v-list density="compact" border elevation="12" class="rounded-lg pa-1">
|
||||
<v-list
|
||||
density="compact"
|
||||
border
|
||||
elevation="12"
|
||||
class="rounded-lg pa-1"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="platform in platformDetails"
|
||||
:key="platform.name"
|
||||
min-height="24"
|
||||
class="px-2"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-avatar size="14" class="mr-2" v-if="platform.icon">
|
||||
<v-img :src="platform.icon"></v-img>
|
||||
<template #prepend>
|
||||
<v-avatar
|
||||
v-if="platform.icon"
|
||||
size="14"
|
||||
class="mr-2"
|
||||
>
|
||||
<v-img :src="platform.icon" />
|
||||
</v-avatar>
|
||||
<v-icon v-else icon="mdi-platform" size="12" class="mr-2"></v-icon>
|
||||
<v-icon
|
||||
v-else
|
||||
icon="mdi-platform"
|
||||
size="12"
|
||||
class="mr-2"
|
||||
/>
|
||||
</template>
|
||||
<v-list-item-title class="text-caption font-weight-bold" style="font-size: 0.75rem !important">
|
||||
<v-list-item-title
|
||||
class="text-caption font-weight-bold"
|
||||
style="font-size: 0.75rem !important"
|
||||
>
|
||||
{{ platform.name }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
@@ -3,73 +3,113 @@
|
||||
<!-- 顶部操作区域 -->
|
||||
<div class="d-flex align-center justify-space-between mb-2">
|
||||
<div class="flex-grow-1">
|
||||
<span v-if="!modelValue || modelValue.length === 0" style="color: rgb(var(--v-theme-primaryText));">
|
||||
<span
|
||||
v-if="!modelValue || modelValue.length === 0"
|
||||
style="color: rgb(var(--v-theme-primaryText));"
|
||||
>
|
||||
{{ tm('pluginSetSelector.notSelected') }}
|
||||
</span>
|
||||
<span v-else-if="isAllPlugins" style="color: rgb(var(--v-theme-primaryText));">
|
||||
<span
|
||||
v-else-if="isAllPlugins"
|
||||
style="color: rgb(var(--v-theme-primaryText));"
|
||||
>
|
||||
{{ tm('pluginSetSelector.allPlugins') }}
|
||||
</span>
|
||||
<span v-else style="color: rgb(var(--v-theme-primaryText));">
|
||||
<span
|
||||
v-else
|
||||
style="color: rgb(var(--v-theme-primaryText));"
|
||||
>
|
||||
{{ tm('pluginSetSelector.selectedCount', { count: modelValue.length }) }}
|
||||
</span>
|
||||
</div>
|
||||
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
|
||||
<v-btn
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
@click="openDialog"
|
||||
>
|
||||
{{ buttonText || tm('pluginSetSelector.buttonText') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plugin Set Selection Dialog -->
|
||||
<v-dialog v-model="dialog" max-width="700px">
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
max-width="700px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
|
||||
<v-card-title
|
||||
class="text-h3 py-4"
|
||||
style="font-weight: normal;"
|
||||
>
|
||||
{{ tm('pluginSetSelector.dialogTitle') }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-4">
|
||||
<v-progress-linear v-if="loading" indeterminate color="primary"></v-progress-linear>
|
||||
<v-progress-linear
|
||||
v-if="loading"
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
|
||||
<div v-if="!loading">
|
||||
<!-- 预设选项 -->
|
||||
<v-radio-group v-model="selectionMode" class="mb-4" hide-details>
|
||||
<v-radio-group
|
||||
v-model="selectionMode"
|
||||
class="mb-4"
|
||||
hide-details
|
||||
>
|
||||
<v-radio
|
||||
value="all"
|
||||
:label="tm('pluginSetSelector.enableAll')"
|
||||
color="primary"
|
||||
></v-radio>
|
||||
/>
|
||||
<v-radio
|
||||
value="none"
|
||||
:label="tm('pluginSetSelector.enableNone')"
|
||||
color="primary"
|
||||
></v-radio>
|
||||
/>
|
||||
<v-radio
|
||||
value="custom"
|
||||
:label="tm('pluginSetSelector.customSelect')"
|
||||
color="primary"
|
||||
></v-radio>
|
||||
/>
|
||||
</v-radio-group>
|
||||
|
||||
<!-- 自定义选择时显示插件列表 -->
|
||||
<div v-if="selectionMode === 'custom'" style="max-height: 300px; overflow-y: auto;">
|
||||
<v-list v-if="pluginList.length > 0" density="compact">
|
||||
<div
|
||||
v-if="selectionMode === 'custom'"
|
||||
style="max-height: 300px; overflow-y: auto;"
|
||||
>
|
||||
<v-list
|
||||
v-if="pluginList.length > 0"
|
||||
density="compact"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="plugin in pluginList"
|
||||
:key="plugin.name"
|
||||
rounded="md"
|
||||
class="ma-1">
|
||||
<template v-slot:prepend>
|
||||
class="ma-1"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-checkbox
|
||||
v-model="selectedPlugins"
|
||||
:value="plugin.name"
|
||||
color="primary"
|
||||
hide-details
|
||||
></v-checkbox>
|
||||
/>
|
||||
</template>
|
||||
|
||||
<v-list-item-title>{{ plugin.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ plugin.desc || tm('pluginSetSelector.noDescription') }}
|
||||
<v-chip v-if="!plugin.activated" size="x-small" color="grey" class="ml-1">
|
||||
<v-chip
|
||||
v-if="!plugin.activated"
|
||||
size="x-small"
|
||||
color="grey"
|
||||
class="ml-1"
|
||||
>
|
||||
{{ tm('pluginSetSelector.notActivated') }}
|
||||
</v-chip>
|
||||
</v-list-item-subtitle>
|
||||
@@ -80,20 +120,36 @@
|
||||
</div>
|
||||
</v-list>
|
||||
|
||||
<div v-else class="text-center py-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-puzzle-outline</v-icon>
|
||||
<p class="text-grey mt-4">{{ tm('pluginSetSelector.noPlugins') }}</p>
|
||||
<div
|
||||
v-else
|
||||
class="text-center py-8"
|
||||
>
|
||||
<v-icon
|
||||
size="64"
|
||||
color="grey-lighten-1"
|
||||
>
|
||||
mdi-puzzle-outline
|
||||
</v-icon>
|
||||
<p class="text-grey mt-4">
|
||||
{{ tm('pluginSetSelector.noPlugins') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="cancelSelection">{{ tm('pluginSetSelector.cancelSelection') }}</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="cancelSelection"
|
||||
>
|
||||
{{ tm('pluginSetSelector.cancelSelection') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="confirmSelection">
|
||||
@click="confirmSelection"
|
||||
>
|
||||
{{ tm('pluginSetSelector.confirmSelection') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
<template>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<span v-if="!hasSelection" style="color: rgb(var(--v-theme-primaryText));">
|
||||
<span
|
||||
v-if="!hasSelection"
|
||||
style="color: rgb(var(--v-theme-primaryText));"
|
||||
>
|
||||
{{ tm('providerSelector.notSelected') }}
|
||||
</span>
|
||||
<span v-else class="provider-name-text">
|
||||
<span
|
||||
v-else
|
||||
class="provider-name-text"
|
||||
>
|
||||
<template v-if="multiple">
|
||||
{{ tm('providerSelector.selectedCount', { count: selectedProviders.length }) }}
|
||||
</template>
|
||||
@@ -11,12 +17,20 @@
|
||||
{{ modelValue }}
|
||||
</template>
|
||||
</span>
|
||||
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
|
||||
<v-btn
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
@click="openDialog"
|
||||
>
|
||||
{{ buttonText || tm('providerSelector.buttonText') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div v-if="multiple && selectedProviders.length > 0" class="selected-preview mt-2">
|
||||
<div
|
||||
v-if="multiple && selectedProviders.length > 0"
|
||||
class="selected-preview mt-2"
|
||||
>
|
||||
<v-chip
|
||||
v-for="providerId in selectedProviders"
|
||||
:key="`preview-${providerId}`"
|
||||
@@ -31,7 +45,10 @@
|
||||
</div>
|
||||
|
||||
<!-- Provider Selection Dialog -->
|
||||
<v-dialog v-model="dialog" max-width="600px">
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
max-width="600px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title
|
||||
class="text-h3 py-4 d-flex align-center justify-space-between gap-4 flex-wrap"
|
||||
@@ -49,14 +66,27 @@
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-0" style="max-height: 400px; overflow-y: auto;">
|
||||
<v-progress-linear v-if="loading" indeterminate color="primary"></v-progress-linear>
|
||||
<v-card-text
|
||||
class="pa-0"
|
||||
style="max-height: 400px; overflow-y: auto;"
|
||||
>
|
||||
<v-progress-linear
|
||||
v-if="loading"
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
|
||||
<div v-if="multiple && selectedProviders.length > 0" class="pa-3">
|
||||
<div
|
||||
v-if="multiple && selectedProviders.length > 0"
|
||||
class="pa-3"
|
||||
>
|
||||
<div class="text-caption text-medium-emphasis mb-2">
|
||||
{{ tm('providerSelector.selectedCount', { count: selectedProviders.length }) }}
|
||||
</div>
|
||||
<v-list density="compact" class="selected-order-list">
|
||||
<v-list
|
||||
density="compact"
|
||||
class="selected-order-list"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(providerId, index) in selectedProviders"
|
||||
:key="`selected-${providerId}-${index}`"
|
||||
@@ -90,63 +120,94 @@
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<v-divider class="ma-1"></v-divider>
|
||||
<v-divider class="ma-1" />
|
||||
</div>
|
||||
|
||||
<v-list v-if="!loading && providerList.length > 0" density="compact">
|
||||
<v-list
|
||||
v-if="!loading && providerList.length > 0"
|
||||
density="compact"
|
||||
>
|
||||
<!-- 不选择选项 -->
|
||||
<v-list-item
|
||||
v-if="!multiple"
|
||||
key="none"
|
||||
value=""
|
||||
@click="selectProvider({ id: '' })"
|
||||
:active="selectedProvider === ''"
|
||||
rounded="md"
|
||||
class="ma-1">
|
||||
class="ma-1"
|
||||
@click="selectProvider({ id: '' })"
|
||||
>
|
||||
<v-list-item-title>{{ tm('providerSelector.clearSelection') }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ tm('providerSelector.clearSelectionSubtitle') }}</v-list-item-subtitle>
|
||||
|
||||
<template v-slot:append>
|
||||
<v-icon v-if="selectedProvider === ''" color="primary">mdi-check-circle</v-icon>
|
||||
<template #append>
|
||||
<v-icon
|
||||
v-if="selectedProvider === ''"
|
||||
color="primary"
|
||||
>
|
||||
mdi-check-circle
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider class="ma-1"></v-divider>
|
||||
<v-divider class="ma-1" />
|
||||
|
||||
<v-list-item
|
||||
v-for="provider in providerList"
|
||||
:key="provider.id"
|
||||
:value="provider.id"
|
||||
@click="selectProvider(provider)"
|
||||
:active="isProviderSelected(provider.id)"
|
||||
rounded="md"
|
||||
class="ma-1">
|
||||
class="ma-1"
|
||||
@click="selectProvider(provider)"
|
||||
>
|
||||
<v-list-item-title>{{ provider.id }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ provider.type || provider.provider_type || tm('providerSelector.unknownType') }}
|
||||
<span v-if="provider.model">- {{ provider.model }}</span>
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<template v-slot:append>
|
||||
<v-icon v-if="isProviderSelected(provider.id)" color="primary">mdi-check-circle</v-icon>
|
||||
<template #append>
|
||||
<v-icon
|
||||
v-if="isProviderSelected(provider.id)"
|
||||
color="primary"
|
||||
>
|
||||
mdi-check-circle
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<div v-else-if="!loading && providerList.length === 0" class="text-center py-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-api-off</v-icon>
|
||||
<p class="text-grey mt-4">{{ tm('providerSelector.noProviders') }}</p>
|
||||
<div
|
||||
v-else-if="!loading && providerList.length === 0"
|
||||
class="text-center py-8"
|
||||
>
|
||||
<v-icon
|
||||
size="64"
|
||||
color="grey-lighten-1"
|
||||
>
|
||||
mdi-api-off
|
||||
</v-icon>
|
||||
<p class="text-grey mt-4">
|
||||
{{ tm('providerSelector.noProviders') }}
|
||||
</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
<v-divider />
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="cancelSelection">{{ tm('providerSelector.cancelSelection') }}</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="cancelSelection"
|
||||
>
|
||||
{{ tm('providerSelector.cancelSelection') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="confirmSelection">
|
||||
@click="confirmSelection"
|
||||
>
|
||||
{{ tm('providerSelector.confirmSelection') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
@@ -161,9 +222,16 @@
|
||||
:scrim="true"
|
||||
@click:outside="closeProviderDrawer"
|
||||
>
|
||||
<v-card class="provider-drawer-card" elevation="12">
|
||||
<v-card
|
||||
class="provider-drawer-card"
|
||||
elevation="12"
|
||||
>
|
||||
<div class="provider-drawer-header">
|
||||
<v-btn icon variant="text" @click="closeProviderDrawer">
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
@click="closeProviderDrawer"
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
@@ -1,51 +1,90 @@
|
||||
<template>
|
||||
<h5>{{ tm('network.proxySelector.title') }}</h5>
|
||||
<v-radio-group class="mt-2" v-model="radioValue" hide-details="true">
|
||||
<v-radio :label="tm('network.proxySelector.noProxy')" value="0"></v-radio>
|
||||
<v-radio value="1">
|
||||
<template v-slot:label>
|
||||
<span>{{ tm('network.proxySelector.useProxy') }}</span>
|
||||
<v-btn v-if="radioValue === '1'" class="ml-2" @click="testAllProxies" size="x-small"
|
||||
variant="tonal" :loading="loadingTestingConnection">
|
||||
{{ tm('network.proxySelector.testConnection') }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<h5>{{ tm('network.proxySelector.title') }}</h5>
|
||||
<v-radio-group
|
||||
v-model="radioValue"
|
||||
class="mt-2"
|
||||
hide-details="true"
|
||||
>
|
||||
<v-radio
|
||||
:label="tm('network.proxySelector.noProxy')"
|
||||
value="0"
|
||||
/>
|
||||
<v-radio value="1">
|
||||
<template #label>
|
||||
<span>{{ tm('network.proxySelector.useProxy') }}</span>
|
||||
<v-btn
|
||||
v-if="radioValue === '1'"
|
||||
class="ml-2"
|
||||
size="x-small"
|
||||
variant="tonal"
|
||||
:loading="loadingTestingConnection"
|
||||
@click="testAllProxies"
|
||||
>
|
||||
{{ tm('network.proxySelector.testConnection') }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-radio>
|
||||
</v-radio-group>
|
||||
<v-expand-transition>
|
||||
<div
|
||||
v-if="radioValue === '1'"
|
||||
style="margin-left: 16px;"
|
||||
>
|
||||
<v-radio-group
|
||||
v-model="githubProxyRadioControl"
|
||||
class="mt-2"
|
||||
hide-details="true"
|
||||
>
|
||||
<v-radio
|
||||
v-for="(proxy, idx) in githubProxies"
|
||||
:key="proxy"
|
||||
color="success"
|
||||
:value="String(idx)"
|
||||
>
|
||||
<template #label>
|
||||
<div class="d-flex align-center">
|
||||
<span class="mr-2">{{ proxy }}</span>
|
||||
<div v-if="proxyStatus[idx]">
|
||||
<v-chip
|
||||
:color="proxyStatus[idx].available ? 'success' : 'error'"
|
||||
size="x-small"
|
||||
class="mr-1"
|
||||
>
|
||||
{{ proxyStatus[idx].available ? tm('network.proxySelector.available') : tm('network.proxySelector.unavailable') }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="proxyStatus[idx].available"
|
||||
color="info"
|
||||
size="x-small"
|
||||
>
|
||||
{{ proxyStatus[idx].latency }}ms
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</v-radio>
|
||||
</v-radio-group>
|
||||
<v-expand-transition>
|
||||
<div v-if="radioValue === '1'" style="margin-left: 16px;">
|
||||
<v-radio-group v-model="githubProxyRadioControl" class="mt-2" hide-details="true">
|
||||
<v-radio color="success" v-for="(proxy, idx) in githubProxies" :key="proxy" :value="String(idx)">
|
||||
<template v-slot:label>
|
||||
<div class="d-flex align-center">
|
||||
<span class="mr-2">{{ proxy }}</span>
|
||||
<div v-if="proxyStatus[idx]">
|
||||
<v-chip
|
||||
:color="proxyStatus[idx].available ? 'success' : 'error'"
|
||||
size="x-small"
|
||||
class="mr-1">
|
||||
{{ proxyStatus[idx].available ? tm('network.proxySelector.available') : tm('network.proxySelector.unavailable') }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="proxyStatus[idx].available"
|
||||
color="info"
|
||||
size="x-small">
|
||||
{{ proxyStatus[idx].latency }}ms
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</v-radio>
|
||||
<v-radio color="primary" value="-1" :label="tm('network.proxySelector.custom')">
|
||||
<template v-slot:label v-if="String(githubProxyRadioControl) === '-1'">
|
||||
<v-text-field density="compact" v-model="selectedGitHubProxy" variant="outlined"
|
||||
style="width: 100vw;" :placeholder="tm('network.proxySelector.custom')" hide-details="true">
|
||||
</v-text-field>
|
||||
</template>
|
||||
</v-radio>
|
||||
</v-radio-group>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
<v-radio
|
||||
color="primary"
|
||||
value="-1"
|
||||
:label="tm('network.proxySelector.custom')"
|
||||
>
|
||||
<template
|
||||
v-if="String(githubProxyRadioControl) === '-1'"
|
||||
#label
|
||||
>
|
||||
<v-text-field
|
||||
v-model="selectedGitHubProxy"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
style="width: 100vw;"
|
||||
:placeholder="tm('network.proxySelector.custom')"
|
||||
hide-details="true"
|
||||
/>
|
||||
</template>
|
||||
</v-radio>
|
||||
</v-radio-group>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -75,6 +114,64 @@ export default {
|
||||
initializing: true,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedGitHubProxy: function (newVal, oldVal) {
|
||||
if (this.initializing) {
|
||||
return;
|
||||
}
|
||||
if (!newVal) {
|
||||
newVal = ""
|
||||
}
|
||||
localStorage.setItem('selectedGitHubProxy', newVal);
|
||||
},
|
||||
radioValue: function (newVal) {
|
||||
if (this.initializing) {
|
||||
return;
|
||||
}
|
||||
localStorage.setItem('githubProxyRadioValue', newVal);
|
||||
if (String(newVal) === "0") {
|
||||
this.selectedGitHubProxy = "";
|
||||
} else if (String(this.githubProxyRadioControl) !== "-1") {
|
||||
this.selectedGitHubProxy = this.getProxyByControl(this.githubProxyRadioControl);
|
||||
}
|
||||
},
|
||||
githubProxyRadioControl: function (newVal) {
|
||||
if (this.initializing) {
|
||||
return;
|
||||
}
|
||||
const normalizedVal = String(newVal);
|
||||
localStorage.setItem('githubProxyRadioControl', normalizedVal);
|
||||
if (String(this.radioValue) !== "1") {
|
||||
this.selectedGitHubProxy = "";
|
||||
return;
|
||||
}
|
||||
if (normalizedVal !== "-1") {
|
||||
this.selectedGitHubProxy = this.getProxyByControl(normalizedVal);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initializing = true;
|
||||
|
||||
const savedProxy = localStorage.getItem('selectedGitHubProxy') || "";
|
||||
const savedRadio = localStorage.getItem('githubProxyRadioValue') || "0";
|
||||
const savedControl = String(localStorage.getItem('githubProxyRadioControl') || "0");
|
||||
|
||||
this.radioValue = savedRadio;
|
||||
this.githubProxyRadioControl = savedControl;
|
||||
|
||||
if (savedRadio === "1") {
|
||||
if (savedControl !== "-1") {
|
||||
this.selectedGitHubProxy = this.getProxyByControl(savedControl);
|
||||
} else {
|
||||
this.selectedGitHubProxy = savedProxy;
|
||||
}
|
||||
} else {
|
||||
this.selectedGitHubProxy = "";
|
||||
}
|
||||
|
||||
this.initializing = false;
|
||||
},
|
||||
methods: {
|
||||
getProxyByControl(control) {
|
||||
const normalizedControl = String(control);
|
||||
@@ -128,64 +225,6 @@ export default {
|
||||
await Promise.all(promises);
|
||||
this.loadingTestingConnection = false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.initializing = true;
|
||||
|
||||
const savedProxy = localStorage.getItem('selectedGitHubProxy') || "";
|
||||
const savedRadio = localStorage.getItem('githubProxyRadioValue') || "0";
|
||||
const savedControl = String(localStorage.getItem('githubProxyRadioControl') || "0");
|
||||
|
||||
this.radioValue = savedRadio;
|
||||
this.githubProxyRadioControl = savedControl;
|
||||
|
||||
if (savedRadio === "1") {
|
||||
if (savedControl !== "-1") {
|
||||
this.selectedGitHubProxy = this.getProxyByControl(savedControl);
|
||||
} else {
|
||||
this.selectedGitHubProxy = savedProxy;
|
||||
}
|
||||
} else {
|
||||
this.selectedGitHubProxy = "";
|
||||
}
|
||||
|
||||
this.initializing = false;
|
||||
},
|
||||
watch: {
|
||||
selectedGitHubProxy: function (newVal, oldVal) {
|
||||
if (this.initializing) {
|
||||
return;
|
||||
}
|
||||
if (!newVal) {
|
||||
newVal = ""
|
||||
}
|
||||
localStorage.setItem('selectedGitHubProxy', newVal);
|
||||
},
|
||||
radioValue: function (newVal) {
|
||||
if (this.initializing) {
|
||||
return;
|
||||
}
|
||||
localStorage.setItem('githubProxyRadioValue', newVal);
|
||||
if (String(newVal) === "0") {
|
||||
this.selectedGitHubProxy = "";
|
||||
} else if (String(this.githubProxyRadioControl) !== "-1") {
|
||||
this.selectedGitHubProxy = this.getProxyByControl(this.githubProxyRadioControl);
|
||||
}
|
||||
},
|
||||
githubProxyRadioControl: function (newVal) {
|
||||
if (this.initializing) {
|
||||
return;
|
||||
}
|
||||
const normalizedVal = String(newVal);
|
||||
localStorage.setItem('githubProxyRadioControl', normalizedVal);
|
||||
if (String(this.radioValue) !== "1") {
|
||||
this.selectedGitHubProxy = "";
|
||||
return;
|
||||
}
|
||||
if (normalizedVal !== "-1") {
|
||||
this.selectedGitHubProxy = this.getProxyByControl(normalizedVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -71,31 +71,30 @@ onUnmounted(() => {
|
||||
if (copyFeedbackTimer.value) clearTimeout(copyFeedbackTimer.value);
|
||||
});
|
||||
|
||||
// 渲染后的 HTML
|
||||
const renderedHtml = computed(() => {
|
||||
// 强制依赖 locale,确保语言切换时重新渲染
|
||||
const _ = locale?.value;
|
||||
if (!content.value) return "";
|
||||
md.renderer.rules.fence = (tokens, idx) => {
|
||||
const token = tokens[idx];
|
||||
const lang = token.info.trim() || "";
|
||||
const code = token.content;
|
||||
|
||||
// 设置 fence 规则,直接使用当前作用域的 t 函数
|
||||
md.renderer.rules.fence = (tokens, idx) => {
|
||||
const token = tokens[idx];
|
||||
const lang = token.info.trim() || "";
|
||||
const code = token.content;
|
||||
const highlighted =
|
||||
lang && hljs.getLanguage(lang)
|
||||
? hljs.highlight(code, { language: lang }).value
|
||||
: md.utils.escapeHtml(code);
|
||||
|
||||
const highlighted =
|
||||
lang && hljs.getLanguage(lang)
|
||||
? hljs.highlight(code, { language: lang }).value
|
||||
: md.utils.escapeHtml(code);
|
||||
|
||||
return `<div class="code-block-wrapper">
|
||||
return `<div class="code-block-wrapper">
|
||||
${lang ? `<span class="code-lang-label">${lang}</span>` : ""}
|
||||
<button class="copy-code-btn" title="${t("core.common.copy")}">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
|
||||
</button>
|
||||
<pre class="hljs"><code class="language-${lang}">${highlighted}</code></pre>
|
||||
</div>`;
|
||||
};
|
||||
};
|
||||
|
||||
// 渲染后的 HTML
|
||||
const renderedHtml = computed(() => {
|
||||
// 强制依赖 locale,确保语言切换时重新渲染
|
||||
const _ = locale?.value;
|
||||
if (!content.value) return "";
|
||||
|
||||
const rawHtml = md.render(content.value);
|
||||
|
||||
@@ -365,16 +364,29 @@ const showActionArea = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog v-model="_show" width="800">
|
||||
<v-dialog
|
||||
v-model="_show"
|
||||
width="800"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<span class="text-h2 pa-2">{{ modeConfig.title }}</span>
|
||||
<v-btn icon @click="_show = false" variant="text">
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
@click="_show = false"
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text ref="scrollContainer" style="overflow-y: auto">
|
||||
<div v-if="showActionArea" class="d-flex justify-space-between mb-4">
|
||||
<v-card-text
|
||||
ref="scrollContainer"
|
||||
style="overflow-y: auto"
|
||||
>
|
||||
<div
|
||||
v-if="showActionArea"
|
||||
class="d-flex justify-space-between mb-4"
|
||||
>
|
||||
<v-btn
|
||||
v-if="modeConfig.showGithubButton && repoUrl"
|
||||
color="primary"
|
||||
@@ -403,25 +415,31 @@ const showActionArea = computed(() => {
|
||||
color="primary"
|
||||
size="64"
|
||||
class="mb-4"
|
||||
></v-progress-circular>
|
||||
<p class="text-body-1 text-center">{{ modeConfig.loading }}</p>
|
||||
/>
|
||||
<p class="text-body-1 text-center">
|
||||
{{ modeConfig.loading }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="renderedHtml"
|
||||
class="markdown-body"
|
||||
v-html="renderedHtml"
|
||||
@click="handleContainerClick"
|
||||
></div>
|
||||
v-html="renderedHtml"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-else-if="error"
|
||||
class="d-flex flex-column align-center justify-center"
|
||||
style="height: 100%"
|
||||
>
|
||||
<v-icon size="64" color="error" class="mb-4"
|
||||
>mdi-alert-circle-outline</v-icon
|
||||
<v-icon
|
||||
size="64"
|
||||
color="error"
|
||||
class="mb-4"
|
||||
>
|
||||
mdi-alert-circle-outline
|
||||
</v-icon>
|
||||
<p class="text-body-1 text-center mb-2">
|
||||
{{ t("core.common.error") }}
|
||||
</p>
|
||||
@@ -435,9 +453,13 @@ const showActionArea = computed(() => {
|
||||
class="d-flex flex-column align-center justify-center"
|
||||
style="height: 100%"
|
||||
>
|
||||
<v-icon size="64" color="warning" class="mb-4"
|
||||
>mdi-file-question-outline</v-icon
|
||||
<v-icon
|
||||
size="64"
|
||||
color="warning"
|
||||
class="mb-4"
|
||||
>
|
||||
mdi-file-question-outline
|
||||
</v-icon>
|
||||
<p class="text-body-1 text-center mb-2">
|
||||
{{ modeConfig.emptyTitle }}
|
||||
</p>
|
||||
@@ -447,8 +469,12 @@ const showActionArea = computed(() => {
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" variant="tonal" @click="_show = false">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
@click="_show = false"
|
||||
>
|
||||
{{ t("core.common.close") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
|
||||
@@ -4,13 +4,16 @@
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
@click="openDialog"
|
||||
style="margin-bottom: 8px;"
|
||||
@click="openDialog"
|
||||
>
|
||||
{{ t('features.settings.sidebar.customize.title') }}
|
||||
</v-btn>
|
||||
|
||||
<v-dialog v-model="dialog" max-width="700px">
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
max-width="700px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<span>{{ t('features.settings.sidebar.customize.title') }}</span>
|
||||
@@ -18,15 +21,22 @@
|
||||
icon="mdi-close"
|
||||
variant="text"
|
||||
@click="dialog = false"
|
||||
></v-btn>
|
||||
/>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<p class="text-body-2 mb-4">{{ t('features.settings.sidebar.customize.subtitle') }}</p>
|
||||
<p class="text-body-2 mb-4">
|
||||
{{ t('features.settings.sidebar.customize.subtitle') }}
|
||||
</p>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<div class="mb-2 font-weight-medium">{{ t('features.settings.sidebar.customize.mainItems') }}</div>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<div class="mb-2 font-weight-medium">
|
||||
{{ t('features.settings.sidebar.customize.mainItems') }}
|
||||
</div>
|
||||
<v-list
|
||||
density="compact"
|
||||
class="custom-list"
|
||||
@@ -42,24 +52,33 @@
|
||||
@dragover.prevent
|
||||
@drop.stop="handleDrop($event, 'main', index)"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon :icon="item.icon" size="small" class="mr-2"></v-icon>
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
:icon="item.icon"
|
||||
size="small"
|
||||
class="mr-2"
|
||||
/>
|
||||
</template>
|
||||
<v-list-item-title>{{ t(item.title) }}</v-list-item-title>
|
||||
<template v-slot:append>
|
||||
<template #append>
|
||||
<v-btn
|
||||
icon="mdi-arrow-right"
|
||||
variant="text"
|
||||
size="x-small"
|
||||
@click="moveToMore(index)"
|
||||
></v-btn>
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<div class="mb-2 font-weight-medium">{{ t('features.settings.sidebar.customize.moreItems') }}</div>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<div class="mb-2 font-weight-medium">
|
||||
{{ t('features.settings.sidebar.customize.moreItems') }}
|
||||
</div>
|
||||
<v-list
|
||||
density="compact"
|
||||
class="custom-list"
|
||||
@@ -75,17 +94,21 @@
|
||||
@dragover.prevent
|
||||
@drop.stop="handleDrop($event, 'more', index)"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon :icon="item.icon" size="small" class="mr-2"></v-icon>
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
:icon="item.icon"
|
||||
size="small"
|
||||
class="mr-2"
|
||||
/>
|
||||
</template>
|
||||
<v-list-item-title>{{ t(item.title) }}</v-list-item-title>
|
||||
<template v-slot:append>
|
||||
<template #append>
|
||||
<v-btn
|
||||
icon="mdi-arrow-left"
|
||||
variant="text"
|
||||
size="x-small"
|
||||
@click="moveToMain(index)"
|
||||
></v-btn>
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
@@ -101,7 +124,7 @@
|
||||
>
|
||||
{{ t('features.settings.sidebar.customize.reset') }}
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="saveCustomization"
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
<template>
|
||||
<v-menu v-bind="$attrs" :close-on-content-click="closeOnContentClick">
|
||||
<template v-slot:activator="{ props: activatorProps }">
|
||||
<slot name="activator" :props="activatorProps"></slot>
|
||||
<v-menu
|
||||
v-bind="$attrs"
|
||||
:close-on-content-click="closeOnContentClick"
|
||||
>
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<slot
|
||||
name="activator"
|
||||
:props="activatorProps"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<v-card class="styled-menu-card" elevation="8" rounded="lg">
|
||||
<v-list density="compact" class="styled-menu-list pa-1">
|
||||
<slot></slot>
|
||||
<v-card
|
||||
class="styled-menu-card"
|
||||
elevation="8"
|
||||
rounded="lg"
|
||||
>
|
||||
<v-list
|
||||
density="compact"
|
||||
class="styled-menu-list pa-1"
|
||||
>
|
||||
<slot />
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<v-dialog v-model="dialog" max-width="1400px" persistent scrollable>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
max-width="1400px"
|
||||
persistent
|
||||
scrollable
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
variant="outlined"
|
||||
@@ -15,8 +20,11 @@
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center justify-space-between">
|
||||
<span>{{ tm('t2iTemplateEditor.dialogTitle') }}</span>
|
||||
<v-spacer></v-spacer>
|
||||
<div class="d-flex align-center gap-2" style="width: 60%">
|
||||
<v-spacer />
|
||||
<div
|
||||
class="d-flex align-center gap-2"
|
||||
style="width: 60%"
|
||||
>
|
||||
<v-text-field
|
||||
v-if="isCreatingNew"
|
||||
v-model="editingName"
|
||||
@@ -27,7 +35,7 @@
|
||||
class="flex-grow-1"
|
||||
autofocus
|
||||
:rules="[v => !!v || tm('t2iTemplateEditor.nameRequired')]"
|
||||
></v-text-field>
|
||||
/>
|
||||
<v-select
|
||||
v-else
|
||||
v-model="selectedTemplate"
|
||||
@@ -41,9 +49,12 @@
|
||||
class="flex-grow-1"
|
||||
:loading="loading"
|
||||
>
|
||||
<template v-slot:item="{ props, item }">
|
||||
<v-list-item v-bind="props" :title="item.raw.name">
|
||||
<template v-slot:append>
|
||||
<template #item="{ props, item }">
|
||||
<v-list-item
|
||||
v-bind="props"
|
||||
:title="item.raw.name"
|
||||
>
|
||||
<template #append>
|
||||
<v-chip
|
||||
v-if="item.raw.name === activeTemplate"
|
||||
color="success"
|
||||
@@ -59,8 +70,8 @@
|
||||
color="primary"
|
||||
size="small"
|
||||
class="ml-2"
|
||||
@click.stop="setActiveTemplate(item.raw.name)"
|
||||
:loading="applyLoading"
|
||||
@click.stop="setActiveTemplate(item.raw.name)"
|
||||
>
|
||||
{{ tm('t2iTemplateEditor.apply') }}
|
||||
</v-btn>
|
||||
@@ -79,55 +90,80 @@
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-0">
|
||||
<v-row no-gutters style="height: 70vh;">
|
||||
<v-row
|
||||
no-gutters
|
||||
style="height: 70vh;"
|
||||
>
|
||||
<!-- 左侧编辑器 -->
|
||||
<v-col cols="6" class="d-flex flex-column">
|
||||
<v-toolbar density="compact" color="surface-variant">
|
||||
<v-toolbar-title class="text-subtitle-2">{{ tm('t2iTemplateEditor.templateEditor') }}</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<div class="d-flex align-center pa-1" style="border: 1px solid rgba(0,0,0,0.1); border-radius: 8px;">
|
||||
<v-col
|
||||
cols="6"
|
||||
class="d-flex flex-column"
|
||||
>
|
||||
<v-toolbar
|
||||
density="compact"
|
||||
color="surface-variant"
|
||||
>
|
||||
<v-toolbar-title class="text-subtitle-2">
|
||||
{{ tm('t2iTemplateEditor.templateEditor') }}
|
||||
</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<div
|
||||
class="d-flex align-center pa-1"
|
||||
style="border: 1px solid rgba(0,0,0,0.1); border-radius: 8px;"
|
||||
>
|
||||
<v-btn
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="newTemplate"
|
||||
color="success"
|
||||
@click="newTemplate"
|
||||
>
|
||||
<v-icon left>mdi-plus</v-icon>
|
||||
<v-icon left>
|
||||
mdi-plus
|
||||
</v-icon>
|
||||
{{ tm('t2iTemplateEditor.new') }}
|
||||
</v-btn>
|
||||
<v-divider vertical class="mx-1"></v-divider>
|
||||
<v-divider
|
||||
vertical
|
||||
class="mx-1"
|
||||
/>
|
||||
<v-btn
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="resetToDefault"
|
||||
:loading="resetLoading"
|
||||
color="warning"
|
||||
@click="resetToDefault"
|
||||
>
|
||||
{{ tm('t2iTemplateEditor.resetBase') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="promptDelete"
|
||||
color="error"
|
||||
:disabled="isCreatingNew || selectedTemplate === 'base' || !selectedTemplate"
|
||||
@click="promptDelete"
|
||||
>
|
||||
{{ tm('t2iTemplateEditor.delete') }}
|
||||
</v-btn>
|
||||
<v-divider vertical class="mx-1"></v-divider>
|
||||
<v-divider
|
||||
vertical
|
||||
class="mx-1"
|
||||
/>
|
||||
<v-btn
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="saveTemplate"
|
||||
:loading="saveLoading"
|
||||
color="primary"
|
||||
:disabled="(isCreatingNew && !editingName) || (!isCreatingNew && !selectedTemplate)"
|
||||
@click="saveTemplate"
|
||||
>
|
||||
{{ tm('t2iTemplateEditor.save') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-toolbar>
|
||||
<div class="flex-grow-1" style="border-right: 1px solid rgba(0,0,0,0.1);">
|
||||
<div
|
||||
class="flex-grow-1"
|
||||
style="border-right: 1px solid rgba(0,0,0,0.1);"
|
||||
>
|
||||
<VueMonacoEditor
|
||||
v-model:value="templateContent"
|
||||
:theme="editorTheme"
|
||||
@@ -139,15 +175,23 @@
|
||||
</v-col>
|
||||
|
||||
<!-- 右侧预览 -->
|
||||
<v-col cols="6" class="d-flex flex-column">
|
||||
<v-toolbar density="compact" color="surface-variant">
|
||||
<v-toolbar-title class="text-subtitle-2">{{ tm('t2iTemplateEditor.livePreview') }}</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col
|
||||
cols="6"
|
||||
class="d-flex flex-column"
|
||||
>
|
||||
<v-toolbar
|
||||
density="compact"
|
||||
color="surface-variant"
|
||||
>
|
||||
<v-toolbar-title class="text-subtitle-2">
|
||||
{{ tm('t2iTemplateEditor.livePreview') }}
|
||||
</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="refreshPreview"
|
||||
:loading="previewLoading"
|
||||
@click="refreshPreview"
|
||||
>
|
||||
{{ tm('t2iTemplateEditor.refreshPreview') }}
|
||||
</v-btn>
|
||||
@@ -164,10 +208,18 @@
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="px-6 py-4">
|
||||
<v-row no-gutters class="align-center">
|
||||
<v-row
|
||||
no-gutters
|
||||
class="align-center"
|
||||
>
|
||||
<v-col>
|
||||
<div class="text-caption text-grey">
|
||||
<v-icon size="16" class="mr-1">mdi-information</v-icon>
|
||||
<v-icon
|
||||
size="16"
|
||||
class="mr-1"
|
||||
>
|
||||
mdi-information
|
||||
</v-icon>
|
||||
{{ tm('t2iTemplateEditor.syntaxHint') }}
|
||||
</div>
|
||||
</v-col>
|
||||
@@ -180,9 +232,9 @@
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="promptApplyAndClose"
|
||||
:loading="saveLoading"
|
||||
:disabled="isCreatingNew || !selectedTemplate"
|
||||
@click="promptApplyAndClose"
|
||||
>
|
||||
{{ tm('t2iTemplateEditor.saveAndApply') }}
|
||||
</v-btn>
|
||||
@@ -192,50 +244,91 @@
|
||||
</v-card>
|
||||
|
||||
<!-- 确认重置对话框 -->
|
||||
<v-dialog v-model="resetDialog" max-width="400px">
|
||||
<v-dialog
|
||||
v-model="resetDialog"
|
||||
max-width="400px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>{{ tm('t2iTemplateEditor.confirmReset') }}</v-card-title>
|
||||
<v-card-text>
|
||||
{{ tm('t2iTemplateEditor.confirmResetMessage') }}
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text @click="resetDialog = false">{{ t('core.common.cancel') }}</v-btn>
|
||||
<v-btn color="warning" @click="confirmReset" :loading="resetLoading">{{ tm('t2iTemplateEditor.confirmResetButton') }}</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
@click="resetDialog = false"
|
||||
>
|
||||
{{ t('core.common.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="warning"
|
||||
:loading="resetLoading"
|
||||
@click="confirmReset"
|
||||
>
|
||||
{{ tm('t2iTemplateEditor.confirmResetButton') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 删除确认对话框 -->
|
||||
<v-dialog v-model="deleteDialog" max-width="400px">
|
||||
<v-dialog
|
||||
v-model="deleteDialog"
|
||||
max-width="400px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>{{ tm('t2iTemplateEditor.confirmDelete') }}</v-card-title>
|
||||
<v-card-text>
|
||||
{{ tm('t2iTemplateEditor.confirmDeleteMessage', { name: selectedTemplate }) }}
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text @click="deleteDialog = false">{{ t('core.common.cancel') }}</v-btn>
|
||||
<v-btn color="error" @click="confirmDelete" :loading="saveLoading">{{ tm('t2iTemplateEditor.confirmDeleteButton') }}</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
@click="deleteDialog = false"
|
||||
>
|
||||
{{ t('core.common.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
:loading="saveLoading"
|
||||
@click="confirmDelete"
|
||||
>
|
||||
{{ tm('t2iTemplateEditor.confirmDeleteButton') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 保存并应用确认对话框 -->
|
||||
<v-dialog v-model="applyAndCloseDialog" max-width="500px">
|
||||
<v-dialog
|
||||
v-model="applyAndCloseDialog"
|
||||
max-width="500px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>{{ tm('t2iTemplateEditor.confirmAction') }}</v-card-title>
|
||||
<v-card-text>
|
||||
{{ tm('t2iTemplateEditor.confirmApplyMessage', { name: selectedTemplate }) }}
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text @click="applyAndCloseDialog = false">{{ t('core.common.cancel') }}</v-btn>
|
||||
<v-btn color="primary" @click="confirmApplyAndClose" :loading="saveLoading">{{ t('core.common.confirm') }}</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
@click="applyAndCloseDialog = false"
|
||||
>
|
||||
{{ t('core.common.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
:loading="saveLoading"
|
||||
@click="confirmApplyAndClose"
|
||||
>
|
||||
{{ t('core.common.confirm') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
@click="addEntry(option.value)"
|
||||
>
|
||||
<v-list-item-title>{{ translateIfKey(option.label) }}</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="option.hint">{{ translateIfKey(option.hint) }}</v-list-item-subtitle>
|
||||
<v-list-item-subtitle v-if="option.hint">
|
||||
{{ translateIfKey(option.hint) }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
@@ -56,25 +58,54 @@
|
||||
<v-icon>{{ expandedEntries[entryIndex] ? 'mdi-chevron-down' : 'mdi-chevron-right' }}</v-icon>
|
||||
</v-btn>
|
||||
<div class="d-flex flex-column">
|
||||
<v-list-item-title class="property-name">{{ templateLabel(entry.__template_key) }}</v-list-item-title>
|
||||
<v-list-item-subtitle class="property-hint" v-if="getTemplate(entry)?.hint || getTemplate(entry)?.description">
|
||||
<v-list-item-title class="property-name">
|
||||
{{ templateLabel(entry.__template_key) }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle
|
||||
v-if="getTemplate(entry)?.hint || getTemplate(entry)?.description"
|
||||
class="property-hint"
|
||||
>
|
||||
{{ translateIfKey(getTemplate(entry)?.hint || getTemplate(entry)?.description) }}
|
||||
</v-list-item-subtitle>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-center ga-1">
|
||||
<v-btn icon size="small" variant="text" color="error" @click.stop="removeEntry(entryIndex)">
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
variant="text"
|
||||
color="error"
|
||||
@click.stop="removeEntry(entryIndex)"
|
||||
>
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-title>
|
||||
<v-expand-transition>
|
||||
<v-card-text v-show="expandedEntries[entryIndex]" class="px-0 py-1">
|
||||
<div v-if="!getTemplate(entry)" class="px-4 py-2">
|
||||
<v-alert type="error" variant="tonal" density="compact">{{ t('core.common.templateList.missingTemplate') || '找不到对应模板,请删除后重新添加。' }}</v-alert>
|
||||
<v-card-text
|
||||
v-show="expandedEntries[entryIndex]"
|
||||
class="px-0 py-1"
|
||||
>
|
||||
<div
|
||||
v-if="!getTemplate(entry)"
|
||||
class="px-4 py-2"
|
||||
>
|
||||
<v-alert
|
||||
type="error"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
>
|
||||
{{ t('core.common.templateList.missingTemplate') || '找不到对应模板,请删除后重新添加。' }}
|
||||
</v-alert>
|
||||
</div>
|
||||
<div v-else class="template-entry-body">
|
||||
<template v-for="(itemMeta, itemKey, metaIndex) in getTemplate(entry).items" :key="itemKey">
|
||||
<div
|
||||
v-else
|
||||
class="template-entry-body"
|
||||
>
|
||||
<template
|
||||
v-for="(itemMeta, itemKey, metaIndex) in getTemplate(entry).items"
|
||||
:key="itemKey"
|
||||
>
|
||||
<!-- Nested Object -->
|
||||
<div
|
||||
v-if="itemMeta?.type === 'object' && !itemMeta?.invisible && shouldShowItem(itemMeta, entry)"
|
||||
@@ -84,14 +115,24 @@
|
||||
<v-list-item-title class="config-title">
|
||||
{{ translateIfKey(itemMeta?.description) || itemKey }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="config-hint" v-if="itemMeta?.hint">
|
||||
<v-list-item-subtitle
|
||||
v-if="itemMeta?.hint"
|
||||
class="config-hint"
|
||||
>
|
||||
{{ translateIfKey(itemMeta.hint) }}
|
||||
</v-list-item-subtitle>
|
||||
</div>
|
||||
<div v-for="(childMeta, childKey, childIndex) in itemMeta.items" :key="childKey">
|
||||
<div
|
||||
v-for="(childMeta, childKey, childIndex) in itemMeta.items"
|
||||
:key="childKey"
|
||||
>
|
||||
<template v-if="!childMeta?.invisible && shouldShowItem(childMeta, entry)">
|
||||
<v-row class="config-row">
|
||||
<v-col cols="12" sm="6" class="property-info">
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
class="property-info"
|
||||
>
|
||||
<v-list-item density="compact">
|
||||
<v-list-item-title class="property-name">
|
||||
{{ translateIfKey(childMeta?.description) || childKey }}
|
||||
@@ -101,7 +142,11 @@
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" class="config-input">
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
class="config-input"
|
||||
>
|
||||
<ConfigItemRenderer
|
||||
v-model="entry[itemKey][childKey]"
|
||||
:item-meta="childMeta"
|
||||
@@ -111,7 +156,7 @@
|
||||
<v-divider
|
||||
v-if="hasVisibleItemsAfter(Object.entries(itemMeta.items), childIndex, entry)"
|
||||
class="config-divider"
|
||||
></v-divider>
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -119,7 +164,11 @@
|
||||
<!-- Regular Property -->
|
||||
<template v-else-if="!itemMeta?.invisible && shouldShowItem(itemMeta, entry)">
|
||||
<v-row class="config-row">
|
||||
<v-col cols="12" sm="6" class="property-info">
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
class="property-info"
|
||||
>
|
||||
<v-list-item density="compact">
|
||||
<v-list-item-title class="property-name">
|
||||
<span v-if="itemMeta?.description">{{ translateIfKey(itemMeta?.description) }} <span class="property-key">({{ itemKey }})</span></span>
|
||||
@@ -130,7 +179,11 @@
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" class="config-input">
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
class="config-input"
|
||||
>
|
||||
<ConfigItemRenderer
|
||||
v-model="entry[itemKey]"
|
||||
:item-meta="itemMeta"
|
||||
@@ -140,7 +193,7 @@
|
||||
<v-divider
|
||||
v-if="hasVisibleItemsAfter(Object.entries(getTemplate(entry).items), metaIndex, entry)"
|
||||
class="config-divider"
|
||||
></v-divider>
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -5,27 +5,52 @@ import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
|
||||
<template>
|
||||
<div class="trace-wrapper">
|
||||
<div class="trace-table" ref="scrollEl" :style="{ height: tableHeight }">
|
||||
<div
|
||||
ref="scrollEl"
|
||||
class="trace-table"
|
||||
:style="{ height: tableHeight }"
|
||||
>
|
||||
<div class="trace-row trace-header">
|
||||
<div class="trace-cell time">Time</div>
|
||||
<div class="trace-cell span">Event ID</div>
|
||||
<div class="trace-cell umo">UMO</div>
|
||||
<div class="trace-cell time">
|
||||
Time
|
||||
</div>
|
||||
<div class="trace-cell span">
|
||||
Event ID
|
||||
</div>
|
||||
<div class="trace-cell umo">
|
||||
UMO
|
||||
</div>
|
||||
<!-- <div class="trace-cell count">Records</div> -->
|
||||
<!-- <div class="trace-cell last">Last</div> -->
|
||||
<div class="trace-cell sender">Sender</div>
|
||||
<div class="trace-cell outline">Outline</div>
|
||||
<div class="trace-cell fields"></div>
|
||||
<div class="trace-cell sender">
|
||||
Sender
|
||||
</div>
|
||||
<div class="trace-cell outline">
|
||||
Outline
|
||||
</div>
|
||||
<div class="trace-cell fields" />
|
||||
</div>
|
||||
<div class="trace-group" :class="{ highlight: highlightMap[event.span_id] }" v-for="event in events"
|
||||
:key="event.span_id">
|
||||
<div
|
||||
v-for="event in events"
|
||||
:key="event.span_id"
|
||||
class="trace-group"
|
||||
:class="{ highlight: highlightMap[event.span_id] }"
|
||||
>
|
||||
<div class="trace-row trace-event">
|
||||
<div class="trace-cell time">{{ formatTime(event.first_time) }}</div>
|
||||
<div class="trace-cell span" :title="event.span_id">
|
||||
<div class="trace-cell time">
|
||||
{{ formatTime(event.first_time) }}
|
||||
</div>
|
||||
<div
|
||||
class="trace-cell span"
|
||||
:title="event.span_id"
|
||||
>
|
||||
<div class="event-title">
|
||||
{{ shortSpan(event.span_id) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="trace-cell umo">{{ event.umo }}</div>
|
||||
<div class="trace-cell umo">
|
||||
{{ event.umo }}
|
||||
</div>
|
||||
<!-- <div class="trace-cell count">
|
||||
<div class="event-meta">{{ event.records.length }}</div>
|
||||
</div> -->
|
||||
@@ -33,33 +58,72 @@ import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
<div class="event-meta">{{ formatTime(event.last_time) }}</div>
|
||||
</div> -->
|
||||
<div class="trace-cell sender">
|
||||
<div class="event-sub" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{{
|
||||
event.sender_name || '-' }}</div>
|
||||
<div
|
||||
class="event-sub"
|
||||
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"
|
||||
>
|
||||
{{
|
||||
event.sender_name || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="trace-cell outline">
|
||||
<div class="event-sub outline">{{ event.message_outline || '-' }}</div>
|
||||
<div class="event-sub outline">
|
||||
{{ event.message_outline || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="trace-cell fields event-controls">
|
||||
<v-btn size="x-small" variant="text" color="primary" @click="toggleEvent(event.span_id)">
|
||||
<v-btn
|
||||
size="x-small"
|
||||
variant="text"
|
||||
color="primary"
|
||||
@click="toggleEvent(event.span_id)"
|
||||
>
|
||||
{{ event.collapsed ? 'Expand' : 'Collapse' }}
|
||||
<span v-if="event.hasAgentPrepare" class="agent-dot" />
|
||||
<span
|
||||
v-if="event.hasAgentPrepare"
|
||||
class="agent-dot"
|
||||
/>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div class="trace-records" v-if="!event.collapsed">
|
||||
<div class="trace-record" v-for="record in getVisibleRecords(event)" :key="record.key">
|
||||
<div class="trace-record-time">{{ record.timeLabel }}</div>
|
||||
<div class="trace-record-action">{{ record.action }}</div>
|
||||
<div
|
||||
v-if="!event.collapsed"
|
||||
class="trace-records"
|
||||
>
|
||||
<div
|
||||
v-for="record in getVisibleRecords(event)"
|
||||
:key="record.key"
|
||||
class="trace-record"
|
||||
>
|
||||
<div class="trace-record-time">
|
||||
{{ record.timeLabel }}
|
||||
</div>
|
||||
<div class="trace-record-action">
|
||||
{{ record.action }}
|
||||
</div>
|
||||
<pre class="trace-record-fields">{{ record.fieldsText }}</pre>
|
||||
</div>
|
||||
<div class="event-more" v-if="event.visibleCount < event.records.length">
|
||||
<v-btn size="x-small" variant="tonal" color="primary" @click="showMore(event.span_id)">
|
||||
<div
|
||||
v-if="event.visibleCount < event.records.length"
|
||||
class="event-more"
|
||||
>
|
||||
<v-btn
|
||||
size="x-small"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
@click="showMore(event.span_id)"
|
||||
>
|
||||
Show more
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="events.length === 0" class="trace-empty">No trace data yet.</div>
|
||||
<div
|
||||
v-if="events.length === 0"
|
||||
class="trace-empty"
|
||||
>
|
||||
No trace data yet.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -15,9 +15,11 @@
|
||||
{{ tm('dialogs.uninstall.message') }}
|
||||
</div>
|
||||
|
||||
<v-divider class="my-4"></v-divider>
|
||||
<v-divider class="my-4" />
|
||||
|
||||
<div class="text-subtitle-2 mb-3">{{ t('core.common.actions') }}:</div>
|
||||
<div class="text-subtitle-2 mb-3">
|
||||
{{ t('core.common.actions') }}:
|
||||
</div>
|
||||
|
||||
<v-checkbox
|
||||
v-model="deleteConfig"
|
||||
@@ -26,10 +28,16 @@
|
||||
hide-details
|
||||
class="mb-2"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<template #append>
|
||||
<v-tooltip location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon v-bind="props" size="small" color="grey">mdi-information-outline</v-icon>
|
||||
<template #activator="{ props }">
|
||||
<v-icon
|
||||
v-bind="props"
|
||||
size="small"
|
||||
color="grey"
|
||||
>
|
||||
mdi-information-outline
|
||||
</v-icon>
|
||||
</template>
|
||||
<span>{{ tm('dialogs.uninstall.configHint') }}</span>
|
||||
</v-tooltip>
|
||||
@@ -42,10 +50,16 @@
|
||||
color="error"
|
||||
hide-details
|
||||
>
|
||||
<template v-slot:append>
|
||||
<template #append>
|
||||
<v-tooltip location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon v-bind="props" size="small" color="grey">mdi-information-outline</v-icon>
|
||||
<template #activator="{ props }">
|
||||
<v-icon
|
||||
v-bind="props"
|
||||
size="small"
|
||||
color="grey"
|
||||
>
|
||||
mdi-information-outline
|
||||
</v-icon>
|
||||
</template>
|
||||
<span>{{ tm('dialogs.uninstall.dataHint') }}</span>
|
||||
</v-tooltip>
|
||||
@@ -59,7 +73,7 @@
|
||||
density="compact"
|
||||
class="mt-4"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-alert</v-icon>
|
||||
</template>
|
||||
{{ t('messages.validation.operation_cannot_be_undone') }}
|
||||
@@ -67,7 +81,7 @@
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="grey"
|
||||
variant="text"
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
<template>
|
||||
<v-dialog v-model="visible" persistent max-width="400">
|
||||
<v-card>
|
||||
<v-card-title>{{ t('core.common.restart.waiting') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-progress-linear indeterminate color="primary"></v-progress-linear>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-dialog
|
||||
v-model="visible"
|
||||
persistent
|
||||
max-width="400"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>{{ t('core.common.restart.waiting') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-progress-linear
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface Conversation {
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
export function useConversations(chatboxMode: boolean = false) {
|
||||
export function useConversations(chatboxMode = false) {
|
||||
const router = useRouter();
|
||||
const conversations = ref<Conversation[]>([]);
|
||||
const selectedConversations = ref<string[]>([]);
|
||||
|
||||
@@ -628,7 +628,7 @@ export function useMessages(
|
||||
// 如果 message 是字符串 (旧格式),转换为数组格式
|
||||
if (typeof message === 'string') {
|
||||
const parts: MessagePart[] = [];
|
||||
let text = message;
|
||||
const text = message;
|
||||
|
||||
// 处理旧格式的特殊标记
|
||||
if (text.startsWith('[IMAGE]')) {
|
||||
@@ -695,7 +695,7 @@ export function useMessages(
|
||||
|
||||
const response = await axios.get('/api/chat/get_session?session_id=' + sessionId);
|
||||
isConvRunning.value = response.data.data.is_running || false;
|
||||
let history = response.data.data.history;
|
||||
const history = response.data.data.history;
|
||||
|
||||
// 保存项目信息(如果存在)
|
||||
currentSessionProject.value = response.data.data.project || null;
|
||||
@@ -714,7 +714,7 @@ export function useMessages(
|
||||
|
||||
// 处理历史消息
|
||||
for (let i = 0; i < history.length; i++) {
|
||||
let content = history[i].content;
|
||||
const content = history[i].content;
|
||||
await parseMessageContent(content);
|
||||
}
|
||||
|
||||
@@ -750,7 +750,7 @@ export function useMessages(
|
||||
const partType = f.type === 'image' ? 'image' :
|
||||
f.type === 'record' ? 'record' : 'file';
|
||||
parts.push({
|
||||
type: partType as 'image' | 'record' | 'file',
|
||||
type: partType ,
|
||||
attachment_id: f.attachment_id
|
||||
});
|
||||
}
|
||||
@@ -793,7 +793,7 @@ export function useMessages(
|
||||
|
||||
isStreaming.value = true;
|
||||
|
||||
while (true) {
|
||||
for (;;) {
|
||||
try {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
@@ -807,7 +807,7 @@ export function useMessages(
|
||||
const lines = chunk.split('\n\n');
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let line = lines[i].trim();
|
||||
const line = lines[i].trim();
|
||||
if (!line) continue;
|
||||
|
||||
let chunkJson: StreamChunk;
|
||||
@@ -917,7 +917,7 @@ export function useMessages(
|
||||
const embeddedUrl = await getAttachment(f.attachment_id);
|
||||
|
||||
userMessageParts.push({
|
||||
type: partType as 'image' | 'record' | 'file',
|
||||
type: partType ,
|
||||
attachment_id: f.attachment_id,
|
||||
filename: f.original_name,
|
||||
embedded_url: partType !== 'file' ? embeddedUrl : undefined,
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface Session {
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export function useSessions(chatboxMode: boolean = false) {
|
||||
export function useSessions(chatboxMode = false) {
|
||||
const router = useRouter();
|
||||
const sessions = ref<Session[]>([]);
|
||||
const selectedSessions = ref<string[]>([]);
|
||||
|
||||
@@ -10,7 +10,7 @@ export function generateMissingKeys(
|
||||
): string[] {
|
||||
const missing: string[] = [];
|
||||
|
||||
function traverse(source: any, target: any, path: string = '') {
|
||||
function traverse(source: any, target: any, path = '') {
|
||||
for (const key in source) {
|
||||
const currentPath = path ? `${path}.${key}` : key;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import type { ValidationResult, ValidationError, UsageReport, TranslationStats } from './types';
|
||||
|
||||
export class I18nValidator {
|
||||
private baseLocale: string = 'zh-CN';
|
||||
private baseLocale = 'zh-CN';
|
||||
private supportedLocales: string[] = ['zh-CN', 'en-US'];
|
||||
|
||||
/**
|
||||
@@ -232,7 +232,7 @@ export class I18nValidator {
|
||||
/**
|
||||
* 获取对象的所有键路径
|
||||
*/
|
||||
private getAllKeys(obj: any, prefix: string = ''): string[] {
|
||||
private getAllKeys(obj: any, prefix = ''): string[] {
|
||||
const keys: string[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
|
||||
@@ -96,7 +96,8 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<v-locale-provider>
|
||||
<v-app :theme="useCustomizerStore().uiTheme"
|
||||
<v-app
|
||||
:theme="useCustomizerStore().uiTheme"
|
||||
:class="[customizer.fontTheme, customizer.mini_sidebar ? 'mini-sidebar' : '', customizer.inputBg ? 'inputWithbg' : '']"
|
||||
>
|
||||
<v-progress-linear
|
||||
@@ -110,10 +111,12 @@ onMounted(() => {
|
||||
/>
|
||||
<VerticalHeaderVue />
|
||||
<VerticalSidebarVue v-if="showSidebar" />
|
||||
<v-main :style="{
|
||||
height: showChatPage ? 'calc(100vh - 55px)' : undefined,
|
||||
overflow: showChatPage ? 'hidden' : undefined
|
||||
}">
|
||||
<v-main
|
||||
:style="{
|
||||
height: showChatPage ? 'calc(100vh - 55px)' : undefined,
|
||||
overflow: showChatPage ? 'hidden' : undefined
|
||||
}"
|
||||
>
|
||||
<v-container
|
||||
fluid
|
||||
class="page-wrapper"
|
||||
@@ -122,9 +125,13 @@ onMounted(() => {
|
||||
height: showChatPage ? '100%' : 'calc(100% - 8px)',
|
||||
padding: (isChatPage || showChatPage) ? '0' : undefined,
|
||||
minHeight: showChatPage ? 'unset' : undefined
|
||||
}">
|
||||
}"
|
||||
>
|
||||
<div :style="{ height: '100%', width: '100%', overflow: showChatPage ? 'hidden' : undefined }">
|
||||
<div v-if="showChatPage" style="height: 100%; width: 100%; overflow: hidden;">
|
||||
<div
|
||||
v-if="showChatPage"
|
||||
style="height: 100%; width: 100%; overflow: hidden;"
|
||||
>
|
||||
<Chat />
|
||||
</div>
|
||||
<RouterView v-else />
|
||||
|
||||
@@ -76,7 +76,7 @@ const desktopUpdateCurrentVersion = ref("-");
|
||||
const desktopUpdateLatestVersion = ref("-");
|
||||
const desktopUpdateStatus = ref("");
|
||||
|
||||
const getAppUpdaterBridge = (): AstrBotAppUpdaterBridge | null => {
|
||||
const getAppUpdaterBridge = (): NonNullable<Window["astrbotAppUpdater"]> | null => {
|
||||
if (typeof window === "undefined") {
|
||||
return null;
|
||||
}
|
||||
@@ -533,7 +533,13 @@ const isChristmas = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-app-bar elevation="0" :priority="0" height="70" class="px-0" app>
|
||||
<v-app-bar
|
||||
elevation="0"
|
||||
:priority="0"
|
||||
height="70"
|
||||
class="px-0"
|
||||
app
|
||||
>
|
||||
<div class="fill-height d-flex align-center w-100 px-4">
|
||||
<!-- 桌面端标题栏拖拽区域 -->
|
||||
<div
|
||||
@@ -547,7 +553,7 @@ const isChristmas = computed(() => {
|
||||
height: 30px;
|
||||
z-index: 9999;
|
||||
"
|
||||
></div>
|
||||
/>
|
||||
|
||||
<div class="d-flex align-center">
|
||||
<!-- 桌面端 menu 按钮 - 仅在 bot 模式下显示 -->
|
||||
@@ -582,22 +588,18 @@ const isChristmas = computed(() => {
|
||||
}"
|
||||
@click="handleLogoClick"
|
||||
>
|
||||
<span class="logo-text Outfit"
|
||||
>Astr<span class="logo-text bot-text-wrapper"
|
||||
>Bot
|
||||
<img
|
||||
v-if="isChristmas"
|
||||
src="@/assets/images/xmas-hat.png"
|
||||
alt="Christmas hat"
|
||||
class="xmas-hat"
|
||||
/> </span
|
||||
></span>
|
||||
<span class="logo-text Outfit">Astr<span class="logo-text bot-text-wrapper">Bot
|
||||
<img
|
||||
v-if="isChristmas"
|
||||
src="@/assets/images/xmas-hat.png"
|
||||
alt="Christmas hat"
|
||||
class="xmas-hat"
|
||||
> </span></span>
|
||||
<span
|
||||
v-if="customizer.viewMode === 'chat'"
|
||||
class="logo-text logo-text-light Outfit"
|
||||
style="color: grey"
|
||||
v-if="customizer.viewMode === 'chat'"
|
||||
>ChatUI</span
|
||||
>
|
||||
>ChatUI</span>
|
||||
<span class="version-text hidden-xs">{{ botCurrVersion }}</span>
|
||||
</div>
|
||||
|
||||
@@ -612,10 +614,18 @@ const isChristmas = computed(() => {
|
||||
group
|
||||
density="compact"
|
||||
>
|
||||
<v-btn value="chat" prepend-icon="mdi-chat-processing-outline">
|
||||
<v-btn
|
||||
value="chat"
|
||||
prepend-icon="mdi-chat-processing-outline"
|
||||
>
|
||||
Chat
|
||||
</v-btn>
|
||||
<v-btn value="bot" prepend-icon="mdi-robot-outline"> Bot </v-btn>
|
||||
<v-btn
|
||||
value="bot"
|
||||
prepend-icon="mdi-robot-outline"
|
||||
>
|
||||
Bot
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
|
||||
@@ -633,8 +643,11 @@ const isChristmas = computed(() => {
|
||||
</div>
|
||||
|
||||
<!-- 功能菜单 -->
|
||||
<StyledMenu offset="12" location="bottom end">
|
||||
<template v-slot:activator="{ props: activatorProps }">
|
||||
<StyledMenu
|
||||
offset="12"
|
||||
location="bottom end"
|
||||
>
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-btn
|
||||
v-bind="activatorProps"
|
||||
size="small"
|
||||
@@ -659,12 +672,22 @@ const isChristmas = computed(() => {
|
||||
color="primary"
|
||||
class="mobile-mode-toggle"
|
||||
>
|
||||
<v-btn value="bot" size="small">
|
||||
<v-icon start>mdi-robot</v-icon>
|
||||
<v-btn
|
||||
value="bot"
|
||||
size="small"
|
||||
>
|
||||
<v-icon start>
|
||||
mdi-robot
|
||||
</v-icon>
|
||||
Bot
|
||||
</v-btn>
|
||||
<v-btn value="chat" size="small">
|
||||
<v-icon start>mdi-chat</v-icon>
|
||||
<v-btn
|
||||
value="chat"
|
||||
size="small"
|
||||
>
|
||||
<v-icon start>
|
||||
mdi-chat
|
||||
</v-icon>
|
||||
Chat
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
@@ -681,25 +704,30 @@ const isChristmas = computed(() => {
|
||||
:location="$vuetify.display.xs ? 'bottom' : 'start center'"
|
||||
offset="8"
|
||||
>
|
||||
<template v-slot:activator="{ props: languageMenuProps }">
|
||||
<template #activator="{ props: languageMenuProps }">
|
||||
<v-list-item
|
||||
v-bind="languageMenuProps"
|
||||
class="styled-menu-item language-group-trigger"
|
||||
rounded="md"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-translate</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{
|
||||
t("core.common.language")
|
||||
}}</v-list-item-title>
|
||||
<template v-slot:append>
|
||||
<v-list-item-title>
|
||||
{{
|
||||
t("core.common.language")
|
||||
}}
|
||||
</v-list-item-title>
|
||||
<template #append>
|
||||
<span class="language-group-current">{{
|
||||
currentLanguage?.flag
|
||||
}}</span>
|
||||
<v-icon size="18" class="language-group-arrow"
|
||||
>mdi-chevron-right</v-icon
|
||||
<v-icon
|
||||
size="18"
|
||||
class="language-group-arrow"
|
||||
>
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
@@ -710,19 +738,22 @@ const isChristmas = computed(() => {
|
||||
elevation="8"
|
||||
rounded="lg"
|
||||
>
|
||||
<v-list density="compact" class="styled-menu-list pa-1">
|
||||
<v-list
|
||||
density="compact"
|
||||
class="styled-menu-list pa-1"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="lang in languages"
|
||||
:key="lang.code"
|
||||
:value="lang.code"
|
||||
@click="changeLanguage(lang.code)"
|
||||
:class="{
|
||||
'styled-menu-item-active': currentLocale === lang.code,
|
||||
}"
|
||||
class="styled-menu-item"
|
||||
rounded="md"
|
||||
@click="changeLanguage(lang.code)"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<template #prepend>
|
||||
<span class="language-flag">{{ lang.flag }}</span>
|
||||
</template>
|
||||
<v-list-item-title>{{ lang.name }}</v-list-item-title>
|
||||
@@ -733,11 +764,11 @@ const isChristmas = computed(() => {
|
||||
|
||||
<!-- 主题切换 -->
|
||||
<v-list-item
|
||||
@click="toggleTheme()"
|
||||
class="styled-menu-item"
|
||||
rounded="md"
|
||||
@click="toggleTheme()"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<template #prepend>
|
||||
<v-icon>
|
||||
{{
|
||||
useCustomizerStore().isDarkTheme
|
||||
@@ -757,54 +788,65 @@ const isChristmas = computed(() => {
|
||||
|
||||
<!-- 更新按钮 -->
|
||||
<v-list-item
|
||||
@click="handleUpdateClick"
|
||||
class="styled-menu-item"
|
||||
rounded="md"
|
||||
@click="handleUpdateClick"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-arrow-up-circle</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{
|
||||
t("core.header.updateDialog.title")
|
||||
}}</v-list-item-title>
|
||||
<v-list-item-title>
|
||||
{{
|
||||
t("core.header.updateDialog.title")
|
||||
}}
|
||||
</v-list-item-title>
|
||||
<template
|
||||
v-slot:append
|
||||
v-if="
|
||||
hasNewVersion || (dashboardHasNewVersion && !isDesktopReleaseMode)
|
||||
"
|
||||
#append
|
||||
>
|
||||
<v-chip size="x-small" color="primary" variant="tonal" class="ml-2"
|
||||
>!</v-chip
|
||||
<v-chip
|
||||
size="x-small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
class="ml-2"
|
||||
>
|
||||
!
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<!-- 账户按钮 -->
|
||||
<v-list-item
|
||||
@click="dialog = true"
|
||||
class="styled-menu-item"
|
||||
rounded="md"
|
||||
@click="dialog = true"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-account</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{
|
||||
t("core.header.accountDialog.title")
|
||||
}}</v-list-item-title>
|
||||
<v-list-item-title>
|
||||
{{
|
||||
t("core.header.accountDialog.title")
|
||||
}}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<!-- 退出登录 -->
|
||||
<v-list-item
|
||||
@click="handleLogout"
|
||||
class="styled-menu-item"
|
||||
rounded="md"
|
||||
@click="handleLogout"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-logout</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{
|
||||
t("core.header.buttons.logout")
|
||||
}}</v-list-item-title>
|
||||
<v-list-item-title>
|
||||
{{
|
||||
t("core.header.buttons.logout")
|
||||
}}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</StyledMenu>
|
||||
</div>
|
||||
@@ -834,10 +876,12 @@ const isChristmas = computed(() => {
|
||||
class="mb-4"
|
||||
indeterminate
|
||||
color="primary"
|
||||
></v-progress-linear>
|
||||
/>
|
||||
|
||||
<div>
|
||||
<h1 style="display: inline-block">{{ botCurrVersion }}</h1>
|
||||
<h1 style="display: inline-block">
|
||||
{{ botCurrVersion }}
|
||||
</h1>
|
||||
<small style="margin-left: 4px">{{ updateStatus }}</small>
|
||||
</div>
|
||||
|
||||
@@ -860,22 +904,18 @@ const isChristmas = computed(() => {
|
||||
</div>
|
||||
|
||||
<div class="mb-4 mt-4">
|
||||
<small
|
||||
>{{ t("core.header.updateDialog.tip") }}
|
||||
{{ t("core.header.updateDialog.tipContinue") }}</small
|
||||
>
|
||||
<small>{{ t("core.header.updateDialog.tip") }}
|
||||
{{ t("core.header.updateDialog.tipContinue") }}</small>
|
||||
</div>
|
||||
|
||||
<!-- 发行版 -->
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<small
|
||||
>{{ t("core.header.updateDialog.dockerTip") }}
|
||||
<small>{{ t("core.header.updateDialog.dockerTip") }}
|
||||
<a href="https://containrrr.dev/watchtower/usage-overview/">{{
|
||||
t("core.header.updateDialog.dockerTipLink")
|
||||
}}</a>
|
||||
{{ t("core.header.updateDialog.dockerTipContinue") }}</small
|
||||
>
|
||||
{{ t("core.header.updateDialog.dockerTipContinue") }}</small>
|
||||
</div>
|
||||
|
||||
<v-alert
|
||||
@@ -886,14 +926,14 @@ const isChristmas = computed(() => {
|
||||
variant="tonal"
|
||||
border="start"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-alert-circle-outline</v-icon>
|
||||
</template>
|
||||
<div class="text-body-2">
|
||||
<strong>{{
|
||||
t("core.header.updateDialog.preReleaseWarning.title")
|
||||
}}</strong>
|
||||
<br />
|
||||
<br>
|
||||
{{
|
||||
t("core.header.updateDialog.preReleaseWarning.description")
|
||||
}}
|
||||
@@ -915,7 +955,7 @@ const isChristmas = computed(() => {
|
||||
item-key="name"
|
||||
:items-per-page="8"
|
||||
>
|
||||
<template v-slot:item.tag_name="{ item }: { item: any }">
|
||||
<template #item.tag_name="{ item }: { item: any }">
|
||||
<div class="d-flex align-center">
|
||||
<span>{{ item.tag_name }}</span>
|
||||
<v-chip
|
||||
@@ -930,29 +970,30 @@ const isChristmas = computed(() => {
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
v-slot:item.body="{
|
||||
#item.body="{
|
||||
item,
|
||||
}: {
|
||||
item: { body: string; tag_name: string };
|
||||
}"
|
||||
>
|
||||
<v-btn
|
||||
@click="openReleaseNotesDialog(item.body, item.tag_name)"
|
||||
rounded="xl"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
size="x-small"
|
||||
>{{ t("core.header.updateDialog.table.view") }}</v-btn
|
||||
@click="openReleaseNotesDialog(item.body, item.tag_name)"
|
||||
>
|
||||
{{ t("core.header.updateDialog.table.view") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<template
|
||||
v-slot:item.switch="{ item }: { item: { tag_name: string } }"
|
||||
#item.switch="{ item }: { item: { tag_name: string } }"
|
||||
>
|
||||
<v-btn
|
||||
@click="switchVersion(item.tag_name)"
|
||||
rounded="xl"
|
||||
variant="plain"
|
||||
color="primary"
|
||||
@click="switchVersion(item.tag_name)"
|
||||
>
|
||||
{{ t("core.header.updateDialog.table.switch") }}
|
||||
</v-btn>
|
||||
@@ -960,19 +1001,17 @@ const isChristmas = computed(() => {
|
||||
</v-data-table>
|
||||
</div>
|
||||
|
||||
<v-divider class="mt-4 mb-4"></v-divider>
|
||||
<v-divider class="mt-4 mb-4" />
|
||||
<div style="margin-top: 16px">
|
||||
<h3 class="mb-4">
|
||||
{{ t("core.header.updateDialog.dashboardUpdate.title") }}
|
||||
</h3>
|
||||
<div class="mb-4">
|
||||
<small
|
||||
>{{
|
||||
t("core.header.updateDialog.dashboardUpdate.currentVersion")
|
||||
}}
|
||||
{{ dashboardCurrentVersion }}</small
|
||||
>
|
||||
<br />
|
||||
<small>{{
|
||||
t("core.header.updateDialog.dashboardUpdate.currentVersion")
|
||||
}}
|
||||
{{ dashboardCurrentVersion }}</small>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
@@ -981,7 +1020,7 @@ const isChristmas = computed(() => {
|
||||
t("core.header.updateDialog.dashboardUpdate.hasNewVersion")
|
||||
}}
|
||||
</p>
|
||||
<p v-else="dashboardHasNewVersion">
|
||||
<p v-else>
|
||||
{{ t("core.header.updateDialog.dashboardUpdate.isLatest") }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -989,9 +1028,9 @@ const isChristmas = computed(() => {
|
||||
<v-btn
|
||||
color="primary"
|
||||
style="border-radius: 10px"
|
||||
@click="updateDashboard()"
|
||||
:disabled="!dashboardHasNewVersion"
|
||||
:loading="updatingDashboardLoading"
|
||||
@click="updateDashboard()"
|
||||
>
|
||||
{{
|
||||
t("core.header.updateDialog.dashboardUpdate.downloadAndUpdate")
|
||||
@@ -1001,7 +1040,7 @@ const isChristmas = computed(() => {
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue-darken-1"
|
||||
variant="text"
|
||||
@@ -1014,7 +1053,10 @@ const isChristmas = computed(() => {
|
||||
</v-dialog>
|
||||
|
||||
<!-- Release Notes Modal -->
|
||||
<v-dialog v-model="releaseNotesDialog" max-width="800">
|
||||
<v-dialog
|
||||
v-model="releaseNotesDialog"
|
||||
max-width="800"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="text-h5">
|
||||
{{ t("core.header.updateDialog.releaseNotes.title") }}:
|
||||
@@ -1028,7 +1070,7 @@ const isChristmas = computed(() => {
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue-darken-1"
|
||||
variant="text"
|
||||
@@ -1040,7 +1082,10 @@ const isChristmas = computed(() => {
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog v-model="desktopUpdateDialog" max-width="460">
|
||||
<v-dialog
|
||||
v-model="desktopUpdateDialog"
|
||||
max-width="460"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="text-h3 pa-4 pl-6 pb-0">
|
||||
{{ t("core.header.updateDialog.desktopApp.title") }}
|
||||
@@ -1049,7 +1094,11 @@ const isChristmas = computed(() => {
|
||||
<div class="mb-3">
|
||||
{{ t("core.header.updateDialog.desktopApp.message") }}
|
||||
</div>
|
||||
<v-alert type="info" variant="tonal" density="compact">
|
||||
<v-alert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
>
|
||||
<div>
|
||||
{{ t("core.header.updateDialog.desktopApp.currentVersion") }}
|
||||
<strong>{{ desktopUpdateCurrentVersion }}</strong>
|
||||
@@ -1073,25 +1122,25 @@ const isChristmas = computed(() => {
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="grey"
|
||||
variant="text"
|
||||
@click="cancelDesktopUpdate"
|
||||
:disabled="desktopUpdateInstalling"
|
||||
@click="cancelDesktopUpdate"
|
||||
>
|
||||
{{ t("core.common.dialog.cancelButton") }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="flat"
|
||||
@click="confirmDesktopUpdate"
|
||||
:loading="desktopUpdateInstalling"
|
||||
:disabled="
|
||||
desktopUpdateChecking ||
|
||||
desktopUpdateInstalling ||
|
||||
!desktopUpdateHasNewVersion
|
||||
desktopUpdateInstalling ||
|
||||
!desktopUpdateHasNewVersion
|
||||
"
|
||||
@click="confirmDesktopUpdate"
|
||||
>
|
||||
{{ t("core.common.dialog.confirmButton") }}
|
||||
</v-btn>
|
||||
@@ -1111,7 +1160,7 @@ const isChristmas = computed(() => {
|
||||
<Logo
|
||||
:title="t('core.header.logoTitle')"
|
||||
:subtitle="t('core.header.accountDialog.title')"
|
||||
></Logo>
|
||||
/>
|
||||
</div>
|
||||
<v-alert
|
||||
v-if="accountWarning"
|
||||
@@ -1143,7 +1192,10 @@ const isChristmas = computed(() => {
|
||||
{{ accountEditStatus.message }}
|
||||
</v-alert>
|
||||
|
||||
<v-form v-model="formValid" @submit.prevent="accountEdit">
|
||||
<v-form
|
||||
v-model="formValid"
|
||||
@submit.prevent="accountEdit"
|
||||
>
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
:append-inner-icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
@@ -1152,11 +1204,11 @@ const isChristmas = computed(() => {
|
||||
variant="outlined"
|
||||
required
|
||||
clearable
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
prepend-inner-icon="mdi-lock-outline"
|
||||
hide-details="auto"
|
||||
class="mb-4"
|
||||
></v-text-field>
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="newPassword"
|
||||
@@ -1166,12 +1218,12 @@ const isChristmas = computed(() => {
|
||||
:label="t('core.header.accountDialog.form.newPassword')"
|
||||
variant="outlined"
|
||||
clearable
|
||||
@click:append-inner="showNewPassword = !showNewPassword"
|
||||
prepend-inner-icon="mdi-lock-plus-outline"
|
||||
:hint="t('core.header.accountDialog.form.passwordHint')"
|
||||
persistent-hint
|
||||
class="mb-4"
|
||||
></v-text-field>
|
||||
@click:append-inner="showNewPassword = !showNewPassword"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="confirmPassword"
|
||||
@@ -1181,12 +1233,12 @@ const isChristmas = computed(() => {
|
||||
:label="t('core.header.accountDialog.form.confirmPassword')"
|
||||
variant="outlined"
|
||||
clearable
|
||||
@click:append-inner="showConfirmPassword = !showConfirmPassword"
|
||||
prepend-inner-icon="mdi-lock-check-outline"
|
||||
:hint="t('core.header.accountDialog.form.confirmPasswordHint')"
|
||||
persistent-hint
|
||||
class="mb-4"
|
||||
></v-text-field>
|
||||
@click:append-inner="showConfirmPassword = !showConfirmPassword"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="newUsername"
|
||||
@@ -1198,7 +1250,7 @@ const isChristmas = computed(() => {
|
||||
:hint="t('core.header.accountDialog.form.usernameHint')"
|
||||
persistent-hint
|
||||
class="mb-3"
|
||||
></v-text-field>
|
||||
/>
|
||||
</v-form>
|
||||
|
||||
<div class="text-caption text-medium-emphasis mt-2">
|
||||
@@ -1206,25 +1258,25 @@ const isChristmas = computed(() => {
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
<v-divider />
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="!accountWarning"
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
@click="dialog = false"
|
||||
:disabled="accountEditStatus.loading"
|
||||
@click="dialog = false"
|
||||
>
|
||||
{{ t("core.header.accountDialog.actions.cancel") }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="accountEdit"
|
||||
:loading="accountEditStatus.loading"
|
||||
:disabled="!formValid"
|
||||
prepend-icon="mdi-content-save"
|
||||
@click="accountEdit"
|
||||
>
|
||||
{{ t("core.header.accountDialog.actions.save") }}
|
||||
</v-btn>
|
||||
@@ -1233,7 +1285,10 @@ const isChristmas = computed(() => {
|
||||
</v-dialog>
|
||||
|
||||
<!-- About 对话框 - 仅在 chat mode 下使用 -->
|
||||
<v-dialog v-model="aboutDialog" width="600">
|
||||
<v-dialog
|
||||
v-model="aboutDialog"
|
||||
width="600"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-text style="overflow-y: auto">
|
||||
<AboutPage />
|
||||
|
||||
@@ -27,10 +27,20 @@ const isItemActive = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-list-group v-if="item.children" :value="item.title" :class="{ 'group-bordered': customizer.mini_sidebar }">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-list-item v-bind="props" rounded class="mb-1" color="secondary" :prepend-icon="item.icon"
|
||||
:style="{ '--indent-padding': '0px' }">
|
||||
<v-list-group
|
||||
v-if="item.children"
|
||||
:value="item.title"
|
||||
:class="{ 'group-bordered': customizer.mini_sidebar }"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<v-list-item
|
||||
v-bind="props"
|
||||
rounded
|
||||
class="mb-1"
|
||||
color="secondary"
|
||||
:prepend-icon="item.icon"
|
||||
:style="{ '--indent-padding': '0px' }"
|
||||
>
|
||||
<v-list-item-title style="font-size: 14px; font-weight: 500; line-height: 1.2; word-break: break-word;">
|
||||
{{ t(item.title) }}
|
||||
</v-list-item-title>
|
||||
@@ -38,24 +48,57 @@ const isItemActive = computed(() => {
|
||||
</template>
|
||||
|
||||
<!-- children -->
|
||||
<template v-for="(child, index) in item.children" :key="child.title || child.to || `child-${index}`">
|
||||
<NavItem :item="child" :level="(level || 0) + 1" />
|
||||
<template
|
||||
v-for="(child, index) in item.children"
|
||||
:key="child.title || child.to || `child-${index}`"
|
||||
>
|
||||
<NavItem
|
||||
:item="child"
|
||||
:level="(level || 0) + 1"
|
||||
/>
|
||||
</template>
|
||||
</v-list-group>
|
||||
|
||||
<v-list-item v-else :to="item.type === 'external' ? '' : item.to" :href="item.type === 'external' ? item.to : ''"
|
||||
:active="isItemActive" rounded class="mb-1" color="secondary" :disabled="item.disabled"
|
||||
:target="item.type === 'external' ? '_blank' : ''" :style="itemStyle">
|
||||
<template v-slot:prepend>
|
||||
<v-icon v-if="item.icon" :size="item.iconSize" class="hide-menu" :icon="item.icon"></v-icon>
|
||||
<v-list-item
|
||||
v-else
|
||||
:to="item.type === 'external' ? '' : item.to"
|
||||
:href="item.type === 'external' ? item.to : ''"
|
||||
:active="isItemActive"
|
||||
rounded
|
||||
class="mb-1"
|
||||
color="secondary"
|
||||
:disabled="item.disabled"
|
||||
:target="item.type === 'external' ? '_blank' : ''"
|
||||
:style="itemStyle"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
v-if="item.icon"
|
||||
:size="item.iconSize"
|
||||
class="hide-menu"
|
||||
:icon="item.icon"
|
||||
/>
|
||||
</template>
|
||||
<v-list-item-title style="font-size: 14px;">{{ t(item.title) }}</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="item.subCaption" class="text-caption mt-n1 hide-menu">
|
||||
<v-list-item-title style="font-size: 14px;">
|
||||
{{ t(item.title) }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle
|
||||
v-if="item.subCaption"
|
||||
class="text-caption mt-n1 hide-menu"
|
||||
>
|
||||
{{ item.subCaption }}
|
||||
</v-list-item-subtitle>
|
||||
<template v-slot:append v-if="item.chip">
|
||||
<v-chip :color="item.chipColor" class="sidebarchip hide-menu" :size="item.chipIcon ? 'small' : 'default'"
|
||||
:variant="item.chipVariant" :prepend-icon="item.chipIcon">
|
||||
<template
|
||||
v-if="item.chip"
|
||||
#append
|
||||
>
|
||||
<v-chip
|
||||
:color="item.chipColor"
|
||||
class="sidebarchip hide-menu"
|
||||
:size="item.chipIcon ? 'small' : 'default'"
|
||||
:variant="item.chipVariant"
|
||||
:prepend-icon="item.chipIcon"
|
||||
>
|
||||
{{ item.chip }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
@@ -278,8 +278,8 @@ function openChangelogDialog() {
|
||||
|
||||
<template>
|
||||
<v-navigation-drawer
|
||||
left
|
||||
v-model="customizer.Sidebar_drawer"
|
||||
left
|
||||
elevation="0"
|
||||
rail-width="80"
|
||||
app
|
||||
@@ -288,37 +288,79 @@ function openChangelogDialog() {
|
||||
:rail="customizer.mini_sidebar"
|
||||
>
|
||||
<div class="sidebar-container">
|
||||
<v-list :class="['pa-4', 'listitem', 'flex-grow-1', { 'hidden-scrollbar': customizer.mini_sidebar }]" v-model:opened="openedItems" :open-strategy="'multiple'">
|
||||
<template v-for="(item, i) in sidebarMenu" :key="item.title || item.to || `sidebar-item-${i}`">
|
||||
<NavItem :item="item" class="leftPadding" />
|
||||
<v-list
|
||||
v-model:opened="openedItems"
|
||||
:class="['pa-4', 'listitem', 'flex-grow-1', { 'hidden-scrollbar': customizer.mini_sidebar }]"
|
||||
:open-strategy="'multiple'"
|
||||
>
|
||||
<template
|
||||
v-for="(item, i) in sidebarMenu"
|
||||
:key="item.title || item.to || `sidebar-item-${i}`"
|
||||
>
|
||||
<NavItem
|
||||
:item="item"
|
||||
class="leftPadding"
|
||||
/>
|
||||
</template>
|
||||
</v-list>
|
||||
<div class="sidebar-footer" v-if="!customizer.mini_sidebar">
|
||||
<v-btn class="sidebar-footer-btn" size="small" variant="tonal" color="primary" to="/settings" prepend-icon="mdi-cog">
|
||||
<div
|
||||
v-if="!customizer.mini_sidebar"
|
||||
class="sidebar-footer"
|
||||
>
|
||||
<v-btn
|
||||
class="sidebar-footer-btn"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
to="/settings"
|
||||
prepend-icon="mdi-cog"
|
||||
>
|
||||
{{ t('core.navigation.settings') }}
|
||||
</v-btn>
|
||||
<v-btn class="sidebar-footer-btn" size="small" variant="text" prepend-icon="mdi-note-text-outline"
|
||||
@click="openChangelogDialog">
|
||||
<v-btn
|
||||
class="sidebar-footer-btn"
|
||||
size="small"
|
||||
variant="text"
|
||||
prepend-icon="mdi-note-text-outline"
|
||||
@click="openChangelogDialog"
|
||||
>
|
||||
{{ t('core.navigation.changelog') }}
|
||||
</v-btn>
|
||||
<v-btn class="sidebar-footer-btn" size="small" variant="text" prepend-icon="mdi-book-open-variant"
|
||||
@click="toggleIframe">
|
||||
<v-btn
|
||||
class="sidebar-footer-btn"
|
||||
size="small"
|
||||
variant="text"
|
||||
prepend-icon="mdi-book-open-variant"
|
||||
@click="toggleIframe"
|
||||
>
|
||||
{{ t('core.navigation.documentation') }}
|
||||
</v-btn>
|
||||
<v-btn class="sidebar-footer-btn" size="small" variant="text" prepend-icon="mdi-frequently-asked-questions"
|
||||
@click="openFaqLink">
|
||||
<v-btn
|
||||
class="sidebar-footer-btn"
|
||||
size="small"
|
||||
variant="text"
|
||||
prepend-icon="mdi-frequently-asked-questions"
|
||||
@click="openFaqLink"
|
||||
>
|
||||
{{ t('core.navigation.faq') }}
|
||||
</v-btn>
|
||||
<v-btn class="sidebar-footer-btn" size="small" variant="text" prepend-icon="mdi-github"
|
||||
@click="openIframeLink('https://github.com/AstrBotDevs/AstrBot')">
|
||||
<v-btn
|
||||
class="sidebar-footer-btn"
|
||||
size="small"
|
||||
variant="text"
|
||||
prepend-icon="mdi-github"
|
||||
@click="openIframeLink('https://github.com/AstrBotDevs/AstrBot')"
|
||||
>
|
||||
{{ t('core.navigation.github') }}
|
||||
<v-chip
|
||||
<v-chip
|
||||
v-if="starCount"
|
||||
size="x-small"
|
||||
variant="outlined"
|
||||
class="ml-2"
|
||||
style="font-weight: normal;"
|
||||
>{{ formatNumber(starCount) }}</v-chip>
|
||||
>
|
||||
{{ formatNumber(starCount) }}
|
||||
</v-chip>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
@@ -326,10 +368,9 @@ function openChangelogDialog() {
|
||||
<div
|
||||
v-if="!customizer.mini_sidebar && customizer.Sidebar_drawer"
|
||||
class="sidebar-resize-handle"
|
||||
@mousedown="startSidebarResize"
|
||||
:class="{ 'resizing': isResizing }"
|
||||
>
|
||||
</div>
|
||||
@mousedown="startSidebarResize"
|
||||
/>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<div
|
||||
@@ -337,8 +378,11 @@ function openChangelogDialog() {
|
||||
id="draggable-iframe"
|
||||
:style="iframeStyle"
|
||||
>
|
||||
|
||||
<div :style="dragHeaderStyle" @mousedown="onMouseDown" @touchstart="onTouchStart">
|
||||
<div
|
||||
:style="dragHeaderStyle"
|
||||
@mousedown="onMouseDown"
|
||||
@touchstart="onTouchStart"
|
||||
>
|
||||
<div style="display: flex; align-items: center;">
|
||||
<v-icon icon="mdi-cursor-move" />
|
||||
<span style="margin-left: 8px;">{{ t('core.navigation.drag') }}</span>
|
||||
@@ -346,17 +390,17 @@ function openChangelogDialog() {
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<v-btn
|
||||
icon
|
||||
style="border-radius: 8px; border: 1px solid #ccc;"
|
||||
@click.stop="openIframeLink('https://astrbot.app')"
|
||||
@mousedown.stop
|
||||
style="border-radius: 8px; border: 1px solid #ccc;"
|
||||
>
|
||||
<v-icon icon="mdi-open-in-new" />
|
||||
</v-btn>
|
||||
<v-btn
|
||||
icon
|
||||
style="border-radius: 8px; border: 1px solid #ccc;"
|
||||
@click.stop="toggleIframe"
|
||||
@mousedown.stop
|
||||
style="border-radius: 8px; border: 1px solid #ccc;"
|
||||
>
|
||||
<v-icon icon="mdi-close" />
|
||||
</v-btn>
|
||||
@@ -365,7 +409,7 @@ function openChangelogDialog() {
|
||||
<iframe
|
||||
src="https://astrbot.app"
|
||||
style="width: 100%; height: calc(100% - 66px); border: none; border-bottom-left-radius: 12px; border-bottom-right-radius: 12px;"
|
||||
></iframe>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 更新日志对话框 -->
|
||||
|
||||
@@ -118,7 +118,7 @@ async function initApp() {
|
||||
const headers = new Headers(
|
||||
init?.headers ||
|
||||
(typeof input !== "string" && "headers" in input
|
||||
? (input as Request).headers
|
||||
? (input ).headers
|
||||
: undefined),
|
||||
);
|
||||
if (token && !headers.has("Authorization")) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import axios from 'axios';
|
||||
export const useAuthStore = defineStore({
|
||||
id: 'auth',
|
||||
state: () => ({
|
||||
// @ts-ignore
|
||||
username: '',
|
||||
returnUrl: null
|
||||
}),
|
||||
|
||||
@@ -4,7 +4,6 @@ import axios from 'axios';
|
||||
export const useCommonStore = defineStore({
|
||||
id: 'common',
|
||||
state: () => ({
|
||||
// @ts-ignore
|
||||
eventSource: null,
|
||||
log_cache: [],
|
||||
sse_connected: false,
|
||||
@@ -141,7 +140,7 @@ export const useCommonStore = defineStore({
|
||||
if (this.startTime !== -1) {
|
||||
return this.startTime
|
||||
}
|
||||
this.fetchStartTime().catch(() => {});
|
||||
this.fetchStartTime().catch(() => undefined);
|
||||
return this.startTime
|
||||
},
|
||||
async getPluginCollections(force = false, customSource = null) {
|
||||
|
||||
@@ -5,7 +5,11 @@ const HAN_IDEOGRAPH_RE = /\p{Unified_Ideograph}/u;
|
||||
export const normalizeStr = (s) => (s ?? "").toString().toLowerCase().trim();
|
||||
|
||||
const normalizeLooseFromNormalized = (normalized) =>
|
||||
normalized.replace(/[\s_-]+/g, "").replace(/[()()【】\[\]{}·•]+/g, "");
|
||||
normalized
|
||||
.replace(/[\s_-]+/g, "")
|
||||
.replace(/[()()【】{}·•]/g, "")
|
||||
.replace(/\[/g, "")
|
||||
.replace(/\]/g, "");
|
||||
|
||||
export const normalizeLoose = (s) =>
|
||||
normalizeLooseFromNormalized(normalizeStr(s));
|
||||
|
||||
@@ -1,23 +1,40 @@
|
||||
<template>
|
||||
<div style="display: flex; flex-direction: column; height: 100%;">
|
||||
<div style="flex-grow: 1; display: flex; align-items: center; justify-content: center; flex-direction: column;">
|
||||
<div style="text-align: center; max-width: 600px;">
|
||||
<h1 class="font-weight-bold">{{ tm('hero.title') }}</h1>
|
||||
<p class="text-subtitle-1" style="color: var(--v-theme-secondaryText);">{{ tm('hero.subtitle') }}</p>
|
||||
<div style="margin-top: 20px; display: flex; justify-content: center;">
|
||||
<v-btn @click="open('https://github.com/AstrBotDevs/AstrBot')" color="primary" variant="tonal" size="small"
|
||||
prepend-icon="mdi-star">
|
||||
{{ tm('hero.starButton') }}
|
||||
</v-btn>
|
||||
<v-btn class="ml-4" @click="open('https://github.com/AstrBotDevs/AstrBot/issues')" color="secondary" size="small"
|
||||
variant="tonal" prepend-icon="mdi-comment-question">
|
||||
{{ tm('hero.issueButton') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; height: 100%;">
|
||||
<div style="flex-grow: 1; display: flex; align-items: center; justify-content: center; flex-direction: column;">
|
||||
<div style="text-align: center; max-width: 600px;">
|
||||
<h1 class="font-weight-bold">
|
||||
{{ tm('hero.title') }}
|
||||
</h1>
|
||||
<p
|
||||
class="text-subtitle-1"
|
||||
style="color: var(--v-theme-secondaryText);"
|
||||
>
|
||||
{{ tm('hero.subtitle') }}
|
||||
</p>
|
||||
<div style="margin-top: 20px; display: flex; justify-content: center;">
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
prepend-icon="mdi-star"
|
||||
@click="open('https://github.com/AstrBotDevs/AstrBot')"
|
||||
>
|
||||
{{ tm('hero.starButton') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="ml-4"
|
||||
color="secondary"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-comment-question"
|
||||
@click="open('https://github.com/AstrBotDevs/AstrBot/issues')"
|
||||
>
|
||||
{{ tm('hero.issueButton') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,35 +1,66 @@
|
||||
<template>
|
||||
<v-card style="height: 100%; width: 100%;">
|
||||
<v-card-text class="pa-4" style="height: 100%;">
|
||||
<v-container fluid class="d-flex flex-column" style="height: 100%;">
|
||||
<v-card-text
|
||||
class="pa-4"
|
||||
style="height: 100%;"
|
||||
>
|
||||
<v-container
|
||||
fluid
|
||||
class="d-flex flex-column"
|
||||
style="height: 100%;"
|
||||
>
|
||||
<div style="margin-bottom: 32px;">
|
||||
<h1 class="gradient-text">{{ tm('page.title') }}</h1>
|
||||
<h1 class="gradient-text">
|
||||
{{ tm('page.title') }}
|
||||
</h1>
|
||||
<small style="color: #a3a3a3;">{{ tm('page.subtitle') }}</small>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap;">
|
||||
<v-btn size="large" :variant="isActive('knowledge-base') ? 'flat' : 'tonal'"
|
||||
:color="isActive('knowledge-base') ? '#9b72cb' : ''" rounded="lg"
|
||||
@click="navigateTo('knowledge-base')">
|
||||
<v-icon start>mdi-text-box-search</v-icon>
|
||||
<v-btn
|
||||
size="large"
|
||||
:variant="isActive('knowledge-base') ? 'flat' : 'tonal'"
|
||||
:color="isActive('knowledge-base') ? '#9b72cb' : ''"
|
||||
rounded="lg"
|
||||
@click="navigateTo('knowledge-base')"
|
||||
>
|
||||
<v-icon start>
|
||||
mdi-text-box-search
|
||||
</v-icon>
|
||||
{{ tm('page.navigation.knowledgeBase') }}
|
||||
</v-btn>
|
||||
<v-btn size="large" :variant="isActive('long-term-memory') ? 'flat' : 'tonal'"
|
||||
:color="isActive('long-term-memory') ? '#9b72cb' : ''" rounded="lg"
|
||||
@click="navigateTo('long-term-memory')">
|
||||
<v-icon start>mdi-dots-hexagon</v-icon>
|
||||
<v-btn
|
||||
size="large"
|
||||
:variant="isActive('long-term-memory') ? 'flat' : 'tonal'"
|
||||
:color="isActive('long-term-memory') ? '#9b72cb' : ''"
|
||||
rounded="lg"
|
||||
@click="navigateTo('long-term-memory')"
|
||||
>
|
||||
<v-icon start>
|
||||
mdi-dots-hexagon
|
||||
</v-icon>
|
||||
{{ tm('page.navigation.longTermMemory') }}
|
||||
</v-btn>
|
||||
<v-btn size="large" :variant="isActive('other') ? 'flat' : 'tonal'"
|
||||
:color="isActive('other') ? '#9b72cb' : ''" rounded="lg"
|
||||
@click="navigateTo('other')">
|
||||
<v-icon start>mdi-tools</v-icon>
|
||||
<v-btn
|
||||
size="large"
|
||||
:variant="isActive('other') ? 'flat' : 'tonal'"
|
||||
:color="isActive('other') ? '#9b72cb' : ''"
|
||||
rounded="lg"
|
||||
@click="navigateTo('other')"
|
||||
>
|
||||
<v-icon start>
|
||||
mdi-tools
|
||||
</v-icon>
|
||||
{{ tm('page.navigation.other') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div id="sub-view" class="flex-grow-1" style="max-height: 100%;">
|
||||
<router-view></router-view>
|
||||
<div
|
||||
id="sub-view"
|
||||
class="flex-grow-1"
|
||||
style="max-height: 100%;"
|
||||
>
|
||||
<router-view />
|
||||
</div>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
@@ -49,6 +80,12 @@ export default {
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
mounted() {
|
||||
// 如果在根路径 /alkaid,默认跳转到知识库页面
|
||||
if (this.$route.path === '/alkaid') {
|
||||
this.navigateTo('knowledge-base');
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
navigateTo(tab) {
|
||||
try {
|
||||
@@ -67,12 +104,6 @@ export default {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 如果在根路径 /alkaid,默认跳转到知识库页面
|
||||
if (this.$route.path === '/alkaid') {
|
||||
this.navigateTo('knowledge-base');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -5,14 +5,18 @@ const customizer = useCustomizerStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-app :theme="customizer.uiTheme" style="height: 100%; width: 100%;">
|
||||
<div
|
||||
style="height: 100%; width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;">
|
||||
<div id="container">
|
||||
<Chat :chatbox-mode="true"></Chat>
|
||||
</div>
|
||||
</div>
|
||||
</v-app>
|
||||
<v-app
|
||||
:theme="customizer.uiTheme"
|
||||
style="height: 100%; width: 100%;"
|
||||
>
|
||||
<div
|
||||
style="height: 100%; width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;"
|
||||
>
|
||||
<div id="container">
|
||||
<Chat :chatbox-mode="true" />
|
||||
</div>
|
||||
</div>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -3,9 +3,9 @@ import Chat from '@/components/chat/Chat.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="chat-container">
|
||||
<Chat />
|
||||
</div>
|
||||
<div class="chat-container">
|
||||
<Chat />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user