feat: streamline project emoji settings

This commit is contained in:
Soulter
2026-09-25 12:35:59 +08:00
parent f5ec23969c
commit 5402af1df5
7 changed files with 160 additions and 106 deletions
@@ -1,14 +1,36 @@
<template>
<v-dialog v-model="isOpen" max-width="640" @update:model-value="handleDialogChange">
<v-dialog v-model="isOpen" max-width="560" @update:model-value="handleDialogChange">
<v-card>
<v-card-title class="text-h3 pa-4 pb-0 pl-6">
{{ isEditing ? tm('project.edit') : tm('project.create') }}
</v-card-title>
<v-card-text>
<v-text-field v-model="form.emoji" :label="tm('project.emoji')" variant="outlined" hide-details class="mb-3" />
<v-text-field v-model="form.title" :label="tm('project.name')" variant="outlined" hide-details class="mb-3" autofocus
@keyup.enter="handleSave" />
<v-textarea v-model="form.description" :label="tm('project.description')" variant="outlined" hide-details rows="3" />
<div class="d-flex align-center ga-2 mb-4">
<EmojiPicker v-model="form.emoji" />
<v-text-field
v-model="form.title"
:label="tm('project.name')"
variant="outlined"
hide-details
class="flex-grow-1"
autofocus
@keyup.enter="handleSave"
/>
</div>
<v-expansion-panels variant="accordion" class="mb-4">
<v-expansion-panel>
<v-expansion-panel-title>{{ tm('project.moreSettings') }}</v-expansion-panel-title>
<v-expansion-panel-text>
<v-textarea
v-model="form.description"
:label="tm('project.description')"
variant="outlined"
hide-details
rows="3"
/>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
<v-divider class="my-4" />
<v-select v-model="form.workspace_type" :items="workspaceTypeItems" item-title="label" item-value="value"
:label="tm('project.workspace.type')" variant="outlined" hide-details class="mb-3" />
@@ -56,6 +78,7 @@
import { computed, ref, watch } from 'vue';
import { useI18n, useModuleI18n } from '@/i18n/composables';
import { getDesktopRuntimeInfo } from '@/utils/desktopRuntime';
import EmojiPicker from '@/components/shared/EmojiPicker.vue';
export type WorkspaceType = 'session' | 'project' | 'custom';
@@ -0,0 +1,124 @@
<template>
<v-menu
v-model="menuOpen"
location="bottom start"
:close-on-content-click="false"
offset="8"
>
<template #activator="{ props: activatorProps }">
<v-btn
v-bind="activatorProps"
class="emoji-picker-trigger"
variant="tonal"
size="large"
:aria-label="tm('emoji.title')"
>
<span aria-hidden="true">{{ modelValue || '📚' }}</span>
<v-icon size="16" class="ms-1">mdi-chevron-down</v-icon>
</v-btn>
</template>
<v-card class="emoji-picker-menu" min-width="300" max-width="340">
<v-card-title class="text-body-1 pa-3">{{ tm('emoji.title') }}</v-card-title>
<v-tabs v-model="activeCategory" density="compact" grow color="primary">
<v-tab
v-for="category in emojiCategories"
:key="category.key"
:value="category.key"
:aria-label="tm(`emoji.categories.${category.key}`)"
>
<span aria-hidden="true">{{ category.icon }}</span>
</v-tab>
</v-tabs>
<v-divider />
<v-card-text class="pa-2">
<div class="emoji-grid">
<v-btn
v-for="emoji in activeEmojis"
:key="emoji"
class="emoji-option"
variant="text"
size="36"
:aria-label="emoji"
@click="selectEmoji(emoji)"
>
{{ emoji }}
</v-btn>
</div>
</v-card-text>
</v-card>
</v-menu>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useModuleI18n } from '@/i18n/composables'
defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const { tm } = useModuleI18n('features/knowledge-base/index')
const menuOpen = ref(false)
const activeCategory = ref('books')
const emojiCategories = [
{
key: 'books',
icon: '📚',
emojis: ['📚', '📖', '📕', '📗', '📘', '📙', '📓', '📔', '📒', '📑', '🗂️', '📂', '📁', '🗃️', '🗄️']
},
{
key: 'emotions',
icon: '🙂',
emojis: ['😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '😉', '😊', '😇', '🥰', '😍']
},
{
key: 'objects',
icon: '💡',
emojis: ['💡', '🔬', '🔭', '🗿', '🏆', '🎯', '🎓', '🔑', '🔒', '🔓', '🔔', '🔕', '🔨', '🛠️', '⚙️']
},
{
key: 'symbols',
icon: '⭐',
emojis: ['❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '⭐', '🌟', '✨', '💫', '⚡', '🔥']
}
]
const activeEmojis = computed(() =>
emojiCategories.find((category) => category.key === activeCategory.value)?.emojis || []
)
function selectEmoji(emoji: string) {
emit('update:modelValue', emoji)
menuOpen.value = false
}
</script>
<style scoped>
.emoji-picker-trigger {
min-width: 56px;
padding-inline: 10px;
font-size: 24px;
}
.emoji-picker-menu {
overflow: hidden;
}
.emoji-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 2px;
}
.emoji-option {
min-width: 0;
padding: 0;
font-size: 22px;
}
</style>
@@ -165,6 +165,7 @@
"name": "Project Name",
"emoji": "Icon (Emoji)",
"description": "Description (Optional)",
"moreSettings": "More settings",
"workspace": {
"type": "Workspace",
"session": "Per-session workspace",
@@ -165,6 +165,7 @@
"name": "プロジェクト名",
"emoji": "アイコン(Emoji)",
"description": "プロジェクトの説明(任意)",
"moreSettings": "その他の設定",
"workspace": {
"type": "ワークスペース",
"session": "セッションごとに分離",
@@ -165,6 +165,7 @@
"name": "Имя проекта",
"emoji": "Иконка (Emoji)",
"description": "Описание проекта (опционально)",
"moreSettings": "Дополнительные настройки",
"workspace": {
"type": "Рабочая область",
"session": "Отдельно для каждого чата",
@@ -165,6 +165,7 @@
"name": "项目名称",
"emoji": "图标 (Emoji)",
"description": "项目描述(可选)",
"moreSettings": "更多设置",
"workspace": {
"type": "工作区",
"session": "按会话分开",
+4 -101
View File
@@ -152,11 +152,9 @@
<v-card-text class="pa-6">
<!-- Emoji 选择器 -->
<div class="text-center mb-6">
<div class="emoji-display" @click="showEmojiPicker = true">
{{ formData.emoji }}
</div>
<p class="text-caption text-medium-emphasis mt-2">{{ t('create.emojiLabel') }}</p>
<div class="d-flex align-center ga-2 mb-4">
<EmojiPicker v-model="formData.emoji" />
<span class="text-body-2 text-medium-emphasis">{{ t('create.emojiLabel') }}</span>
</div>
<!-- 表单 -->
@@ -213,31 +211,6 @@
</v-card>
</v-dialog>
<!-- Emoji 选择器对话框 -->
<v-dialog v-model="showEmojiPicker" max-width="500px">
<v-card>
<v-card-title class="text-h3 pa-4 pb-0 pl-6">{{ t('emoji.title') }}</v-card-title>
<v-divider />
<v-card-text class="pa-4">
<div v-for="category in emojiCategories" :key="category.key" class="mb-4">
<p class="text-subtitle-2 mb-2">{{ t(`emoji.categories.${category.key}`) }}</p>
<div class="emoji-grid">
<div v-for="emoji in category.emojis" :key="emoji" class="emoji-item" @click="selectEmoji(emoji)">
{{ emoji }}
</div>
</div>
</div>
</v-card-text>
<v-divider />
<v-card-actions class="pa-4">
<v-spacer />
<v-btn variant="text" @click="showEmojiPicker = false">
{{ t('emoji.close') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 删除确认对话框 -->
<v-dialog v-model="showDeleteDialog" max-width="450px" persistent>
<v-card>
@@ -280,6 +253,7 @@ import { useRouter } from 'vue-router'
import { knowledgeApi, providerApi } from '@/api/v1'
import { useModuleI18n } from '@/i18n/composables'
import OutlinedActionListItem from '@/components/shared/OutlinedActionListItem.vue'
import EmojiPicker from '@/components/shared/EmojiPicker.vue'
const { tm: t } = useModuleI18n('features/knowledge-base/index')
const router = useRouter()
@@ -302,7 +276,6 @@ const pendingEmbeddingProvider = ref<string | null>(null)
// 对话框
const showCreateDialog = ref(false)
const showEmojiPicker = ref(false)
const showDeleteDialog = ref(false)
// Snackbar 通知
@@ -324,26 +297,6 @@ const formData = ref({
rerank_provider_id: null
})
// Emoji 分类
const emojiCategories = [
{
key: 'books',
emojis: ['📚', '📖', '📕', '📗', '📘', '📙', '📓', '📔', '📒', '📑', '🗂️', '📂', '📁', '🗃️', '🗄️']
},
{
key: 'emotions',
emojis: ['😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '😉', '😊', '😇', '🥰', '😍']
},
{
key: 'objects',
emojis: ['💡', '🔬', '🔭', '🗿', '🏆', '🎯', '🎓', '🔑', '🔒', '🔓', '🔔', '🔕', '🔨', '🛠️', '⚙️']
},
{
key: 'symbols',
emojis: ['❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '⭐', '🌟', '✨', '💫', '⚡', '🔥']
}
]
// 加载知识库列表
const loadKnowledgeBases = async (refreshStats = false) => {
loading.value = true
@@ -509,12 +462,6 @@ const closeCreateDialog = () => {
formRef.value?.reset()
}
// 选择 emoji
const selectEmoji = (emoji: string) => {
formData.value.emoji = emoji
showEmojiPicker.value = false
}
// 显示通知
const showSnackbar = (text: string, color: string = 'success') => {
snackbar.value.text = text
@@ -645,48 +592,4 @@ onMounted(() => {
min-height: 400px;
}
/* Emoji 显示和选择器 */
.emoji-display {
font-size: 72px;
cursor: pointer;
transition: transform 0.2s ease;
display: inline-block;
padding: 0px 16px;
border-radius: 12px;
background: rgba(var(--v-theme-primary), 0.05);
}
.emoji-display:hover {
transform: scale(1.1);
background: rgba(var(--v-theme-primary), 0.1);
}
.emoji-grid {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 8px;
}
.emoji-item {
font-size: 32px;
padding: 12px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 8px;
transition: all 0.2s ease;
}
.emoji-item:hover {
background: rgba(var(--v-theme-primary), 0.1);
transform: scale(1.2);
}
/* 响应式设计 */
@media (max-width: 768px) {
.emoji-grid {
grid-template-columns: repeat(6, 1fr);
}
}
</style>