mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-20 02:55:08 +08:00
同步:同步主线
This commit is contained in:
@@ -1,24 +1,182 @@
|
||||
<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 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="isCompactLayout ? '96vw' : '1000px'"
|
||||
:min-width="isCompactLayout ? undefined : '800px'"
|
||||
>
|
||||
<v-card class="selector-dialog-card">
|
||||
<v-card-title class="dialog-title d-flex align-center" :class="isCompactLayout ? 'py-3 px-4' : '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 selector-content">
|
||||
<div class="selector-layout">
|
||||
<!-- 左侧文件夹树 -->
|
||||
<div v-if="!isCompactLayout" 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 v-if="isCompactLayout" class="mobile-folder-bar px-4 py-2">
|
||||
<v-btn icon="mdi-arrow-left" size="small" variant="text"
|
||||
:disabled="currentFolderId === null" @click="navigateToParentFolder" />
|
||||
<v-btn size="small" variant="tonal" color="primary" prepend-icon="mdi-home"
|
||||
@click="navigateToFolder(null)">
|
||||
{{ labels.rootFolder || '根目录' }}
|
||||
</v-btn>
|
||||
<span class="text-caption text-medium-emphasis text-truncate mobile-folder-label">
|
||||
{{ currentFolderLabel }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<v-divider v-if="isCompactLayout" />
|
||||
|
||||
<!-- 面包屑导航 -->
|
||||
<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>
|
||||
|
||||
<!-- 选择对话框 -->
|
||||
@@ -398,6 +556,18 @@ export default defineComponent({
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isCompactLayout(): boolean {
|
||||
return this.$vuetify.display.smAndDown;
|
||||
},
|
||||
|
||||
currentFolderLabel(): string {
|
||||
if (this.currentFolderId === null) {
|
||||
return this.labels.rootFolder || '根目录';
|
||||
}
|
||||
const currentFolder = this.breadcrumbPath[this.breadcrumbPath.length - 1];
|
||||
return currentFolder?.name || this.labels.rootFolder || '根目录';
|
||||
},
|
||||
|
||||
displayValue(): string {
|
||||
if (this.displayValueFormatter) {
|
||||
return this.displayValueFormatter(this.modelValue);
|
||||
@@ -485,6 +655,20 @@ export default defineComponent({
|
||||
this.$emit('navigate', folderId);
|
||||
},
|
||||
|
||||
navigateToParentFolder() {
|
||||
if (this.currentFolderId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.breadcrumbPath.length <= 1) {
|
||||
this.navigateToFolder(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = this.breadcrumbPath[this.breadcrumbPath.length - 2];
|
||||
this.navigateToFolder(parent?.folder_id ?? null);
|
||||
},
|
||||
|
||||
findFolderInTree(folderId: string): FolderTreeNode | null {
|
||||
const findNode = (nodes: FolderTreeNode[]): FolderTreeNode | null => {
|
||||
for (const node of nodes) {
|
||||
@@ -567,6 +751,13 @@ export default defineComponent({
|
||||
.selector-layout {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.selector-content {
|
||||
height: 600px;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.folder-sidebar {
|
||||
@@ -603,6 +794,18 @@ export default defineComponent({
|
||||
|
||||
.items-content {
|
||||
background-color: transparent;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mobile-folder-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mobile-folder-label {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tree-list {
|
||||
@@ -670,22 +873,28 @@ export default defineComponent({
|
||||
background-color: rgba(var(--v-theme-primary), 0.08);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
@media (max-width: 960px) {
|
||||
.selector-layout {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
max-height: 500px;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.folder-sidebar {
|
||||
width: 100%;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
max-height: 150px;
|
||||
.selector-content {
|
||||
max-height: 76vh;
|
||||
}
|
||||
|
||||
.items-list {
|
||||
max-height: 300px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.breadcrumb-bar {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.breadcrumb-bar :deep(.v-breadcrumbs) {
|
||||
flex-wrap: nowrap;
|
||||
min-width: max-content;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
},
|
||||
"description": {
|
||||
"openai": "Supports all OpenAI API compatible providers.",
|
||||
"kimi_code": "Dedicated Kimi CodingPlan / Kimi Code integration using a custom Anthropic API compatibility layer.",
|
||||
"default": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
},
|
||||
"description": {
|
||||
"openai": "Поддерживаются все провайдеры, совместимые с OpenAI API.",
|
||||
"kimi_code": "Специальная интеграция Kimi CodingPlan / Kimi Code с отдельной совместимой адаптацией Anthropic API.",
|
||||
"vllm_rerank": "Также поддерживает Jina AI, Cohere, PPIO и другие.",
|
||||
"default": "Преобразование речи в текст"
|
||||
}
|
||||
@@ -148,4 +149,4 @@
|
||||
"modelId": "ID модели"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
},
|
||||
"description": {
|
||||
"openai": "也支持所有兼容 OpenAI API 的模型提供商。",
|
||||
"kimi_code": "Kimi 的 CodingPlan / Kimi Code 专用接入,使用特殊的 Anthropic API 兼容适配。",
|
||||
"vllm_rerank": "也支持 Jina AI, Cohere, PPIO 等提供商。",
|
||||
"default": ""
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ export function getProviderIcon(type) {
|
||||
'nvidia': 'https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@latest/icons/nvidia-color.svg',
|
||||
'siliconflow': 'https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@latest/icons/siliconcloud.svg',
|
||||
'moonshot': 'https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@latest/icons/kimi.svg',
|
||||
'kimi': 'https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@latest/icons/kimi.svg',
|
||||
'kimi-code': 'https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@latest/icons/kimi.svg',
|
||||
'ppio': 'https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@latest/icons/ppio.svg',
|
||||
'dify': 'https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@latest/icons/dify-color.svg',
|
||||
"coze": "https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.66.0/icons/coze.svg",
|
||||
@@ -50,9 +52,11 @@ export function getProviderIcon(type) {
|
||||
* @returns {string} 提供商描述
|
||||
*/
|
||||
export function getProviderDescription(template, name, tm) {
|
||||
if (name == 'OpenAI') {
|
||||
if (name === 'OpenAI') {
|
||||
return tm('providers.description.openai', { type: template.type });
|
||||
} else if (name == 'vLLM Rerank') {
|
||||
} else if (template.provider === 'kimi-code') {
|
||||
return tm('providers.description.kimi_code');
|
||||
} else if (name === 'vLLM Rerank') {
|
||||
return tm('providers.description.vllm_rerank', { type: template.type });
|
||||
}
|
||||
return tm('providers.description.default', { type: template.type });
|
||||
|
||||
Reference in New Issue
Block a user