feat(ui): 收藏模块功能国际化、布局优化与组件重构
此提交包含收藏模块的国际化支持、布局和交互优化,以及核心组件的重构,以提升可维护性、用户体验和性能。
**主要特性与改进:**
1. **收藏功能国际化 (i18n):**
* 实现了收藏模块所有组件的三语言(中、英、繁)国际化支持。
* 国际化了 `CategoryTreeSelect.vue`、`FavoriteCard.vue`、`FavoriteManager.vue` 和 `SaveFavoriteDialog.vue` 中的所有硬编码文本。
* 更新了 `zh-CN.ts`、`en-US.ts` 和 `zh-TW.ts` 翻译资源,新增了必要的翻译键。
* 彻底消除了 UI 层的硬编码中文文本。
2. **收藏管理器布局与交互优化:**
* 重构 `FavoriteCard` 为固定高度网格布局,解决了卡片高度不一致问题。
* 新增 `useTooltipTheme` 工具统一 Tooltip 样式,并优化了其智能定位逻辑和空间计算。
* 实现了响应式网格布局 (移动端1列/平板2列/桌面4列),并对 `resize` 事件进行节流和缓存计算结果以优化性能。
* 改进了公开标识的可见性,使用 `NTag` 替代纯图标。
* 将 CSS `v-bind` 改为动态 `style` 绑定提升稳定性。
* `FavoriteManager` 移除了滚动容器,改为固定高度工具栏、可滚动卡片区域与固定底部分页的设计。
* 精简了视图模式,移除了列表视图,仅保留卡片视图,并删除了视图切换按钮组。
* 优化了工具栏布局为单行,左侧包含搜索/筛选/计数,右侧为操作按钮,并支持响应式设计。
* 精简了代码和依赖,删除了多余的组件导入和状态管理,减少了约 180 行代码。
* **修复:** 修复了 `FavoriteManager` 编辑对话框中 `NGrid` 和 `NGridItem` 组件缺失导入导致的运行时警告。
3. **收藏对话框组件重构:**
* 新增了 `CategoryTreeSelect` 通用组件,支持树状分类选择、内置分类管理和自动加载数据。
* 改造 `SaveFavoriteDialog` 为通用组件,支持新建/保存模式,动态标题和字段禁用逻辑,并集成了 `CategoryTreeSelect`。
* 简化了 `FavoriteManager` 组件,删除了 120 行重复代码,统一使用 `SaveFavoriteDialog` 和 `CategoryTreeSelect`。
* 遵循 DRY、KISS、YAGNI 原则,消除了约 150 行重复代码,提升了可维护性。
**Breaking Changes:**
* 收藏管理器布局从响应式 CSS Grid 改为固定网格 + 分页设计。
* 每页显示数量现在根据屏幕尺寸动态调整 (4-8项)。
* 移除了列表视图 (list view),仅保留卡片视图 (grid view)。
This commit is contained in:
1 parent
609f454d6a
commit
5bf02fa4df
9 files changed
+1501
-1138
No files matched your search
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<NTreeSelect
|
||||
v-model:value="internalValue"
|
||||
:options="treeOptions"
|
||||
:placeholder="placeholder || t('favorites.dialog.categoryPlaceholder')"
|
||||
:clearable="clearable"
|
||||
:consistent-menu-width="consistentMenuWidth"
|
||||
:style="computedStyle"
|
||||
@update:value="handleValueChange"
|
||||
>
|
||||
<template v-if="showManageButton" #action>
|
||||
<NButton
|
||||
text
|
||||
block
|
||||
@click="handleOpenManager"
|
||||
style="justify-content: flex-start; padding: 8px 12px;"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon><Folder /></NIcon>
|
||||
</template>
|
||||
{{ t('favorites.manager.categoryManager.title') }}
|
||||
</NButton>
|
||||
</template>
|
||||
</NTreeSelect>
|
||||
|
||||
<!-- 分类管理对话框 -->
|
||||
<NModal
|
||||
v-if="showManageButton"
|
||||
v-model:show="managerVisible"
|
||||
preset="card"
|
||||
:title="t('favorites.manager.categoryManager.title')"
|
||||
:mask-closable="true"
|
||||
:style="{ width: 'min(800px, 90vw)', height: 'min(600px, 80vh)' }"
|
||||
>
|
||||
<CategoryManager @category-updated="handleCategoryUpdated" />
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, inject, type Ref } from 'vue';
|
||||
import { NTreeSelect, NButton, NIcon, NModal, type TreeSelectOption } from 'naive-ui';
|
||||
import { Folder } from '@vicons/tabler';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import CategoryManager from './CategoryManager.vue';
|
||||
import type { FavoriteCategory } from '@prompt-optimizer/core';
|
||||
import type { AppServices } from '../types/services';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
interface Props {
|
||||
/** 当前选中的分类ID */
|
||||
modelValue?: string;
|
||||
/** 占位符文本 */
|
||||
placeholder?: string;
|
||||
/** 是否可清除 */
|
||||
clearable?: boolean;
|
||||
/** 是否显示"全部分类"选项(用于筛选场景) */
|
||||
showAllOption?: boolean;
|
||||
/** 是否显示管理按钮 */
|
||||
showManageButton?: boolean;
|
||||
/** 自定义样式 */
|
||||
style?: string;
|
||||
/** 是否保持菜单宽度一致 */
|
||||
consistentMenuWidth?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: '',
|
||||
placeholder: '',
|
||||
clearable: true,
|
||||
showAllOption: false,
|
||||
showManageButton: true,
|
||||
style: 'min-width: 180px; max-width: 250px;',
|
||||
consistentMenuWidth: true
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string];
|
||||
'change': [value: string];
|
||||
'category-updated': [];
|
||||
}>();
|
||||
|
||||
const services = inject<Ref<AppServices | null> | null>('services', null);
|
||||
|
||||
// 内部状态
|
||||
const internalValue = ref(props.modelValue);
|
||||
const categories = ref<FavoriteCategory[]>([]);
|
||||
const managerVisible = ref(false);
|
||||
|
||||
// 计算树状分类选项
|
||||
const treeOptions = computed<TreeSelectOption[]>(() => {
|
||||
const buildTree = (parentId?: string): TreeSelectOption[] => {
|
||||
return categories.value
|
||||
.filter(cat => cat.parentId === parentId)
|
||||
.map(cat => ({
|
||||
label: cat.name,
|
||||
key: cat.id,
|
||||
children: buildTree(cat.id)
|
||||
}));
|
||||
};
|
||||
|
||||
const tree = buildTree(undefined);
|
||||
|
||||
// 如果是筛选模式,添加"全部分类"选项
|
||||
if (props.showAllOption) {
|
||||
return [
|
||||
{ label: t('favorites.manager.allCategories'), key: '' },
|
||||
...tree
|
||||
];
|
||||
}
|
||||
|
||||
return tree;
|
||||
});
|
||||
|
||||
// 计算样式
|
||||
const computedStyle = computed(() => props.style);
|
||||
|
||||
// 加载分类数据
|
||||
const loadCategories = async () => {
|
||||
const servicesValue = services?.value;
|
||||
if (!servicesValue?.favoriteManager) {
|
||||
console.warn('收藏管理器未初始化,跳过分类加载');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
categories.value = await servicesValue.favoriteManager.getCategories();
|
||||
} catch (error: any) {
|
||||
console.error('加载分类失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理值变化
|
||||
const handleValueChange = (value: string) => {
|
||||
internalValue.value = value;
|
||||
emit('update:modelValue', value);
|
||||
emit('change', value);
|
||||
};
|
||||
|
||||
// 打开分类管理器
|
||||
const handleOpenManager = () => {
|
||||
managerVisible.value = true;
|
||||
};
|
||||
|
||||
// 分类更新后刷新数据
|
||||
const handleCategoryUpdated = async () => {
|
||||
await loadCategories();
|
||||
emit('category-updated');
|
||||
};
|
||||
|
||||
// 监听外部值变化
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
if (newValue !== internalValue.value) {
|
||||
internalValue.value = newValue;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听服务初始化
|
||||
watch(() => services?.value?.favoriteManager, (favoriteManager) => {
|
||||
if (favoriteManager) {
|
||||
loadCategories();
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
reloadCategories: loadCategories
|
||||
});
|
||||
</script>
|
||||
@@ -1,167 +1,234 @@
|
||||
<template>
|
||||
<NCard
|
||||
class="favorite-card"
|
||||
hoverable
|
||||
:class="{ 'is-selected': isSelected }"
|
||||
:content-style="{ padding: '12px' }"
|
||||
:header-style="{ padding: '12px' }"
|
||||
:footer-style="{ padding: '12px' }"
|
||||
:content-style="{ padding: '16px', display: 'flex', flexDirection: 'column', gap: '12px' }"
|
||||
:header-style="{ padding: '12px 16px' }"
|
||||
:footer-style="{ padding: '12px 16px' }"
|
||||
:style="{ minHeight: `${cardHeight}px`, maxHeight: `${cardHeight}px` }"
|
||||
@click="$emit('select', favorite)"
|
||||
>
|
||||
<!-- 卡片头部 -->
|
||||
<!-- 卡片头部:确保内容不溢出 -->
|
||||
<template #header>
|
||||
<NSpace align="center" justify="space-between" :wrap="false" :size="8">
|
||||
<NSpace align="center" justify="space-between" :wrap="false" :size="8" style="overflow: hidden;">
|
||||
<NEllipsis style="flex: 1; min-width: 0; font-weight: 600; font-size: 14px;">
|
||||
{{ favorite.title }}
|
||||
</NEllipsis>
|
||||
<NTag
|
||||
v-if="category"
|
||||
:color="{ color: category.color, textColor: 'white' }"
|
||||
size="small"
|
||||
style="flex-shrink: 0;"
|
||||
>
|
||||
{{ category.name }}
|
||||
</NTag>
|
||||
<div style="flex-shrink: 0; max-width: 120px; overflow: hidden;">
|
||||
<NTag
|
||||
v-if="category"
|
||||
:color="{ color: category.color, textColor: 'white' }"
|
||||
size="small"
|
||||
:bordered="false"
|
||||
style="max-width: 100%;"
|
||||
>
|
||||
<NEllipsis style="max-width: 100px;">
|
||||
{{ category.name }}
|
||||
</NEllipsis>
|
||||
</NTag>
|
||||
</div>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<!-- 卡片内容 -->
|
||||
<NSpace vertical :size="6" class="card-body">
|
||||
<NEllipsis
|
||||
:line-clamp="3"
|
||||
:tooltip="{ width: 400, placement: 'top' }"
|
||||
<!-- 卡片内容:精确控制区域大小,防止溢出 -->
|
||||
<NSpace vertical :size="6" style="flex: 1; min-height: 0; overflow: hidden;">
|
||||
<!-- 内容区域:固定2行 -->
|
||||
<NTooltip
|
||||
trigger="hover"
|
||||
:show-arrow="false"
|
||||
:overlay-style="tooltipOverlayStyle"
|
||||
:content-style="[
|
||||
tooltipContentStyle,
|
||||
contentTooltipMaxHeight
|
||||
? { maxHeight: `${contentTooltipMaxHeight}px` }
|
||||
: null
|
||||
]"
|
||||
:theme-overrides="tooltipThemeOverrides"
|
||||
:placement="contentTooltipPlacement"
|
||||
:width="contentTooltipWidth"
|
||||
to="body"
|
||||
:flip="true"
|
||||
scrollable
|
||||
@update:show="(value) => value && handleContentTooltipEnter()"
|
||||
>
|
||||
<NText style="font-size: 13px; line-height: 1.5;">{{ favorite.content }}</NText>
|
||||
</NEllipsis>
|
||||
<template #trigger>
|
||||
<div
|
||||
ref="contentTriggerRef"
|
||||
style="height: 42px; overflow: hidden;"
|
||||
@mouseenter="handleContentTooltipEnter"
|
||||
>
|
||||
<NEllipsis :line-clamp="2" :tooltip="false">
|
||||
<NText style="font-size: 13px; line-height: 1.5;">
|
||||
{{ favorite.content }}
|
||||
</NText>
|
||||
</NEllipsis>
|
||||
</div>
|
||||
</template>
|
||||
<NText class="tooltip-text">{{ favorite.content }}</NText>
|
||||
</NTooltip>
|
||||
|
||||
<NEllipsis v-if="favorite.description" :line-clamp="1" :tooltip="false">
|
||||
<NText depth="3" style="font-size: 12px; line-height: 1.4;">
|
||||
{{ favorite.description }}
|
||||
</NText>
|
||||
</NEllipsis>
|
||||
<!-- 描述区域:固定1行 -->
|
||||
<NTooltip
|
||||
v-if="favorite.description"
|
||||
trigger="hover"
|
||||
:show-arrow="false"
|
||||
:overlay-style="tooltipOverlayStyle"
|
||||
:content-style="[
|
||||
tooltipContentStyle,
|
||||
descriptionTooltipMaxHeight
|
||||
? { maxHeight: `${descriptionTooltipMaxHeight}px` }
|
||||
: null
|
||||
]"
|
||||
:theme-overrides="tooltipThemeOverrides"
|
||||
:placement="descriptionTooltipPlacement"
|
||||
:width="descriptionTooltipWidth"
|
||||
to="body"
|
||||
:flip="true"
|
||||
scrollable
|
||||
@update:show="(value) => value && handleDescriptionTooltipEnter()"
|
||||
>
|
||||
<template #trigger>
|
||||
<div
|
||||
ref="descriptionTriggerRef"
|
||||
style="height: 20px; overflow: hidden;"
|
||||
@mouseenter="handleDescriptionTooltipEnter"
|
||||
>
|
||||
<NEllipsis :line-clamp="1" :tooltip="false">
|
||||
<NText depth="3" style="font-size: 12px; line-height: 1.4;">
|
||||
{{ favorite.description }}
|
||||
</NText>
|
||||
</NEllipsis>
|
||||
</div>
|
||||
</template>
|
||||
<NText class="tooltip-text">{{ favorite.description }}</NText>
|
||||
</NTooltip>
|
||||
<div v-else style="height: 20px; overflow: hidden;"></div>
|
||||
|
||||
<!-- 标签 -->
|
||||
<NSpace v-if="favorite.tags.length > 0" :size="4" :wrap="false" class="card-tags">
|
||||
<NTag
|
||||
v-for="tag in favorite.tags.slice(0, 3)"
|
||||
:key="tag"
|
||||
size="small"
|
||||
type="info"
|
||||
>
|
||||
{{ tag }}
|
||||
</NTag>
|
||||
<NTag
|
||||
v-if="favorite.tags.length > 3"
|
||||
size="small"
|
||||
type="default"
|
||||
>
|
||||
+{{ favorite.tags.length - 3 }}
|
||||
</NTag>
|
||||
</NSpace>
|
||||
<!-- 标签区域:固定高度,最多显示2个标签 -->
|
||||
<div style="height: 22px; overflow: hidden;">
|
||||
<NSpace v-if="favorite.tags.length > 0" :size="4" :wrap="false">
|
||||
<NTag
|
||||
v-for="tag in favorite.tags.slice(0, 2)"
|
||||
:key="tag"
|
||||
size="small"
|
||||
type="info"
|
||||
:bordered="false"
|
||||
>
|
||||
{{ tag }}
|
||||
</NTag>
|
||||
<NTag
|
||||
v-if="favorite.tags.length > 2"
|
||||
size="small"
|
||||
type="default"
|
||||
:bordered="false"
|
||||
>
|
||||
+{{ favorite.tags.length - 2 }}
|
||||
</NTag>
|
||||
</NSpace>
|
||||
</div>
|
||||
</NSpace>
|
||||
|
||||
<!-- 卡片底部 -->
|
||||
<!-- 卡片底部:使用 NSpace 布局 -->
|
||||
<template #footer>
|
||||
<NSpace justify="space-between" align="center" :wrap="false">
|
||||
<NSpace :size="8" align="center" :wrap="false">
|
||||
<!-- 左侧信息 -->
|
||||
<NSpace :size="12" align="center" :wrap="false">
|
||||
<NText depth="3" style="font-size: 12px; white-space: nowrap;">
|
||||
{{ formatDate(favorite.updatedAt) }}
|
||||
</NText>
|
||||
<NTooltip trigger="hover">
|
||||
<NTooltip
|
||||
trigger="hover"
|
||||
:overlay-style="tooltipOverlayStyle"
|
||||
:content-style="tooltipContentStyle"
|
||||
:theme-overrides="tooltipThemeOverrides"
|
||||
placement="bottom"
|
||||
to="body"
|
||||
:flip="true"
|
||||
>
|
||||
<template #trigger>
|
||||
<NSpace :size="2" align="center" style="cursor: help;">
|
||||
<NSpace :size="4" align="center" style="cursor: help;">
|
||||
<NIcon size="14" depth="3"><Eye /></NIcon>
|
||||
<NText depth="3" style="font-size: 12px;">
|
||||
{{ favorite.useCount }}
|
||||
</NText>
|
||||
<NText depth="3" style="font-size: 12px;">{{ favorite.useCount }}</NText>
|
||||
</NSpace>
|
||||
</template>
|
||||
使用次数
|
||||
{{ t('favorites.manager.card.useCount') }}
|
||||
</NTooltip>
|
||||
</NSpace>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<!-- 右侧操作按钮 -->
|
||||
<NSpace :size="4" class="card-actions">
|
||||
<NTooltip trigger="hover">
|
||||
<NTooltip
|
||||
trigger="hover"
|
||||
:overlay-style="tooltipOverlayStyle"
|
||||
:content-style="tooltipContentStyle"
|
||||
:theme-overrides="tooltipThemeOverrides"
|
||||
placement="bottom"
|
||||
to="body"
|
||||
:flip="true"
|
||||
>
|
||||
<template #trigger>
|
||||
<NButton
|
||||
size="small"
|
||||
quaternary
|
||||
circle
|
||||
@click.stop="$emit('copy', favorite)"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon><Copy /></NIcon>
|
||||
</template>
|
||||
<NButton size="small" quaternary circle @click.stop="$emit('copy', favorite)">
|
||||
<template #icon><NIcon><Copy /></NIcon></template>
|
||||
</NButton>
|
||||
</template>
|
||||
复制内容
|
||||
{{ t('favorites.manager.card.copyContent') }}
|
||||
</NTooltip>
|
||||
|
||||
<NTooltip trigger="hover">
|
||||
<NTooltip
|
||||
trigger="hover"
|
||||
:overlay-style="tooltipOverlayStyle"
|
||||
:content-style="tooltipContentStyle"
|
||||
:theme-overrides="tooltipThemeOverrides"
|
||||
placement="bottom"
|
||||
to="body"
|
||||
:flip="true"
|
||||
>
|
||||
<template #trigger>
|
||||
<NButton
|
||||
size="small"
|
||||
quaternary
|
||||
circle
|
||||
@click.stop="$emit('use', favorite)"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon><PlayerPlay /></NIcon>
|
||||
</template>
|
||||
<NButton size="small" quaternary circle @click.stop="$emit('use', favorite)">
|
||||
<template #icon><NIcon><PlayerPlay /></NIcon></template>
|
||||
</NButton>
|
||||
</template>
|
||||
立即使用
|
||||
{{ t('favorites.manager.card.useNow') }}
|
||||
</NTooltip>
|
||||
|
||||
<NTooltip trigger="hover">
|
||||
<NTooltip
|
||||
trigger="hover"
|
||||
:overlay-style="tooltipOverlayStyle"
|
||||
:content-style="tooltipContentStyle"
|
||||
:theme-overrides="tooltipThemeOverrides"
|
||||
placement="bottom"
|
||||
to="body"
|
||||
:flip="true"
|
||||
>
|
||||
<template #trigger>
|
||||
<NButton
|
||||
size="small"
|
||||
quaternary
|
||||
circle
|
||||
@click.stop="$emit('edit', favorite)"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon><Edit /></NIcon>
|
||||
</template>
|
||||
<NButton size="small" quaternary circle @click.stop="$emit('edit', favorite)">
|
||||
<template #icon><NIcon><Edit /></NIcon></template>
|
||||
</NButton>
|
||||
</template>
|
||||
编辑
|
||||
{{ t('favorites.manager.card.edit') }}
|
||||
</NTooltip>
|
||||
|
||||
<NPopconfirm
|
||||
@positive-click="$emit('delete', favorite)"
|
||||
positive-text="删除"
|
||||
negative-text="取消"
|
||||
:positive-text="t('favorites.manager.card.delete')"
|
||||
:negative-text="t('favorites.manager.card.cancel')"
|
||||
>
|
||||
<template #trigger>
|
||||
<NButton
|
||||
size="small"
|
||||
quaternary
|
||||
circle
|
||||
type="error"
|
||||
@click.stop
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon><Trash /></NIcon>
|
||||
</template>
|
||||
<NButton size="small" quaternary circle type="error" @click.stop>
|
||||
<template #icon><NIcon><Trash /></NIcon></template>
|
||||
</NButton>
|
||||
</template>
|
||||
确定删除"{{ favorite.title }}"吗?
|
||||
{{ t('favorites.manager.card.deleteConfirm', { title: favorite.title }) }}
|
||||
</NPopconfirm>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<!-- 公开标识 -->
|
||||
<div v-if="favorite.isPublic" class="public-badge">
|
||||
<NIcon color="#f39c12" size="16"><Star /></NIcon>
|
||||
</div>
|
||||
</NCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import {
|
||||
NCard,
|
||||
NEllipsis,
|
||||
@@ -181,16 +248,22 @@ import {
|
||||
Edit,
|
||||
Trash
|
||||
} from '@vicons/tabler';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { FavoritePrompt, FavoriteCategory } from '@prompt-optimizer/core';
|
||||
import { useTooltipTheme } from '../composables/useTooltipTheme';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
interface Props {
|
||||
favorite: FavoritePrompt;
|
||||
category?: FavoriteCategory;
|
||||
isSelected?: boolean;
|
||||
cardHeight?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
isSelected: false
|
||||
isSelected: false,
|
||||
cardHeight: 240
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -201,6 +274,224 @@ const emit = defineEmits<{
|
||||
'use': [favorite: FavoritePrompt];
|
||||
}>();
|
||||
|
||||
type TooltipPlacement =
|
||||
| 'bottom-start'
|
||||
| 'bottom-end'
|
||||
| 'bottom'
|
||||
| 'top-start'
|
||||
| 'top-end'
|
||||
| 'top';
|
||||
|
||||
const VIEWPORT_MARGIN = 16;
|
||||
const TOOLTIP_GUTTER = 12;
|
||||
const TOTAL_MARGIN = VIEWPORT_MARGIN + TOOLTIP_GUTTER;
|
||||
const MIN_TOOLTIP_HEIGHT = 160;
|
||||
const CONTENT_TOOLTIP_WIDTH = 440;
|
||||
const CONTENT_TOOLTIP_MIN_WIDTH = 240;
|
||||
const DESCRIPTION_TOOLTIP_WIDTH = 360;
|
||||
const DESCRIPTION_TOOLTIP_MIN_WIDTH = 200;
|
||||
|
||||
const {
|
||||
tooltipThemeOverrides,
|
||||
tooltipOverlayStyle,
|
||||
tooltipContentStyle
|
||||
} = useTooltipTheme({
|
||||
maxWidth: `calc(100vw - ${TOTAL_MARGIN * 2}px)`,
|
||||
maxHeight: `calc(100vh - ${TOTAL_MARGIN * 2}px)`
|
||||
}); // 统一 tooltip 背景并限制整体尺寸
|
||||
|
||||
const contentTooltipPlacement = ref<TooltipPlacement>('bottom-start');
|
||||
const contentTooltipWidth = ref<number>(CONTENT_TOOLTIP_WIDTH);
|
||||
const contentTooltipMaxHeight = ref<number>(0);
|
||||
const descriptionTooltipPlacement = ref<TooltipPlacement>('bottom-start');
|
||||
const descriptionTooltipWidth = ref<number>(DESCRIPTION_TOOLTIP_WIDTH);
|
||||
const descriptionTooltipMaxHeight = ref<number>(0);
|
||||
const contentTriggerRef = ref<HTMLElement | null>(null);
|
||||
const descriptionTriggerRef = ref<HTMLElement | null>(null);
|
||||
|
||||
// 缓存 rect 结果,避免频繁计算
|
||||
const cachedContentRect = ref<DOMRect | null>(null);
|
||||
const cachedDescriptionRect = ref<DOMRect | null>(null);
|
||||
|
||||
const getViewportWidth = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return window.innerWidth;
|
||||
}
|
||||
return 1440;
|
||||
};
|
||||
|
||||
const getViewportHeight = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return window.innerHeight;
|
||||
}
|
||||
return 900;
|
||||
};
|
||||
|
||||
interface TooltipLayoutResult {
|
||||
placement: TooltipPlacement;
|
||||
width: number;
|
||||
maxHeight: number;
|
||||
}
|
||||
|
||||
const calculateTooltipLayout = (rect: DOMRect, desiredWidth: number, minWidth: number): TooltipLayoutResult => {
|
||||
const viewportWidth = getViewportWidth();
|
||||
const viewportHeight = getViewportHeight();
|
||||
const safeAreaWidth = Math.max(viewportWidth - TOTAL_MARGIN * 2, minWidth);
|
||||
const safeAreaHeight = Math.max(viewportHeight - TOTAL_MARGIN * 2, MIN_TOOLTIP_HEIGHT);
|
||||
const targetWidth = Math.min(desiredWidth, safeAreaWidth);
|
||||
|
||||
// 修复空间计算:正确计算左右可用空间
|
||||
// spaceRight: 元素右边缘到视口右边缘的距离
|
||||
// spaceLeft: 元素左边缘到视口左边缘的距离
|
||||
const spaceRight = Math.max(0, viewportWidth - rect.right - TOTAL_MARGIN);
|
||||
const spaceLeft = Math.max(0, rect.left - TOTAL_MARGIN);
|
||||
const spaceDown = Math.max(0, viewportHeight - rect.bottom - TOTAL_MARGIN);
|
||||
const spaceUp = Math.max(0, rect.top - TOTAL_MARGIN);
|
||||
|
||||
if (spaceRight <= 0 && spaceLeft <= 0) {
|
||||
return {
|
||||
placement: spaceDown >= spaceUp ? 'bottom' : 'top',
|
||||
width: targetWidth,
|
||||
maxHeight: safeAreaHeight
|
||||
};
|
||||
}
|
||||
|
||||
let verticalPlacement: 'bottom' | 'top' = spaceDown >= spaceUp ? 'bottom' : 'top';
|
||||
let availableVertical = verticalPlacement === 'bottom' ? spaceDown : spaceUp;
|
||||
|
||||
const oppositeVertical = verticalPlacement === 'bottom' ? spaceUp : spaceDown;
|
||||
if (availableVertical < MIN_TOOLTIP_HEIGHT && oppositeVertical > availableVertical) {
|
||||
verticalPlacement = verticalPlacement === 'bottom' ? 'top' : 'bottom';
|
||||
availableVertical = oppositeVertical;
|
||||
}
|
||||
|
||||
if (availableVertical <= 0) {
|
||||
availableVertical = safeAreaHeight;
|
||||
}
|
||||
|
||||
let placement: TooltipPlacement = verticalPlacement === 'bottom' ? 'bottom-start' : 'top-start';
|
||||
let available = placement.endsWith('end') ? spaceLeft : spaceRight;
|
||||
if (spaceLeft > spaceRight) {
|
||||
placement = verticalPlacement === 'bottom' ? 'bottom-end' : 'top-end';
|
||||
available = spaceLeft;
|
||||
}
|
||||
|
||||
let width = Math.min(targetWidth, available);
|
||||
|
||||
const oppositeAvailable = placement.endsWith('start') ? spaceLeft : spaceRight;
|
||||
if (width < Math.min(minWidth, targetWidth) && oppositeAvailable > available) {
|
||||
placement = placement.endsWith('start')
|
||||
? (verticalPlacement === 'bottom' ? 'bottom-end' : 'top-end')
|
||||
: (verticalPlacement === 'bottom' ? 'bottom-start' : 'top-start');
|
||||
available = oppositeAvailable;
|
||||
width = Math.min(targetWidth, available);
|
||||
}
|
||||
|
||||
const minRequired = Math.min(minWidth, targetWidth, available);
|
||||
if (minRequired > 0 && width < minRequired) {
|
||||
width = minRequired;
|
||||
}
|
||||
|
||||
if (width <= 0) {
|
||||
placement = verticalPlacement;
|
||||
width = targetWidth;
|
||||
available = safeAreaWidth;
|
||||
}
|
||||
|
||||
const finalWidth = placement === 'bottom'
|
||||
? Math.min(width, safeAreaWidth)
|
||||
: Math.min(width, available, safeAreaWidth);
|
||||
|
||||
const maxHeight = Math.max(
|
||||
Math.min(safeAreaHeight, availableVertical),
|
||||
Math.min(safeAreaHeight, MIN_TOOLTIP_HEIGHT)
|
||||
);
|
||||
|
||||
return {
|
||||
placement,
|
||||
width: Math.max(
|
||||
finalWidth,
|
||||
Math.min(safeAreaWidth, minWidth, finalWidth || targetWidth)
|
||||
),
|
||||
maxHeight
|
||||
};
|
||||
};
|
||||
|
||||
const updateContentTooltipLayout = (rect: DOMRect) => {
|
||||
const { placement, width, maxHeight } = calculateTooltipLayout(
|
||||
rect,
|
||||
CONTENT_TOOLTIP_WIDTH,
|
||||
CONTENT_TOOLTIP_MIN_WIDTH
|
||||
);
|
||||
contentTooltipPlacement.value = placement;
|
||||
contentTooltipWidth.value = width;
|
||||
contentTooltipMaxHeight.value = maxHeight;
|
||||
};
|
||||
|
||||
const updateDescriptionTooltipLayout = (rect: DOMRect) => {
|
||||
const { placement, width, maxHeight } = calculateTooltipLayout(
|
||||
rect,
|
||||
DESCRIPTION_TOOLTIP_WIDTH,
|
||||
DESCRIPTION_TOOLTIP_MIN_WIDTH
|
||||
);
|
||||
descriptionTooltipPlacement.value = placement;
|
||||
descriptionTooltipWidth.value = width;
|
||||
descriptionTooltipMaxHeight.value = maxHeight;
|
||||
};
|
||||
|
||||
const handleContentTooltipEnter = () => {
|
||||
const target = contentTriggerRef.value;
|
||||
if (!target) return;
|
||||
|
||||
// 使用缓存的 rect,避免重复计算
|
||||
if (!cachedContentRect.value) {
|
||||
cachedContentRect.value = target.getBoundingClientRect();
|
||||
}
|
||||
updateContentTooltipLayout(cachedContentRect.value);
|
||||
};
|
||||
|
||||
const handleDescriptionTooltipEnter = () => {
|
||||
const target = descriptionTriggerRef.value;
|
||||
if (!target) return;
|
||||
|
||||
// 使用缓存的 rect,避免重复计算
|
||||
if (!cachedDescriptionRect.value) {
|
||||
cachedDescriptionRect.value = target.getBoundingClientRect();
|
||||
}
|
||||
updateDescriptionTooltipLayout(cachedDescriptionRect.value);
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
// resize 时清除缓存并重新计算
|
||||
cachedContentRect.value = null;
|
||||
cachedDescriptionRect.value = null;
|
||||
|
||||
if (contentTriggerRef.value) {
|
||||
cachedContentRect.value = contentTriggerRef.value.getBoundingClientRect();
|
||||
updateContentTooltipLayout(cachedContentRect.value);
|
||||
}
|
||||
if (descriptionTriggerRef.value) {
|
||||
cachedDescriptionRect.value = descriptionTriggerRef.value.getBoundingClientRect();
|
||||
updateDescriptionTooltipLayout(cachedDescriptionRect.value);
|
||||
}
|
||||
};
|
||||
|
||||
// 节流处理 resize 事件,避免频繁计算
|
||||
const debouncedResize = useDebounceFn(handleResize, 150);
|
||||
|
||||
onMounted(() => {
|
||||
handleResize();
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('resize', debouncedResize);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('resize', debouncedResize);
|
||||
}
|
||||
});
|
||||
|
||||
const formatDate = (timestamp: number) => {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
@@ -211,13 +502,13 @@ const formatDate = (timestamp: number) => {
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||
if (hours === 0) {
|
||||
const minutes = Math.floor(diff / (1000 * 60));
|
||||
return minutes <= 1 ? '刚刚' : `${minutes}分钟前`;
|
||||
return minutes <= 1 ? t('favorites.manager.time.justNow') : t('favorites.manager.time.minutesAgo', { minutes });
|
||||
}
|
||||
return `${hours}小时前`;
|
||||
return t('favorites.manager.time.hoursAgo', { hours });
|
||||
} else if (days === 1) {
|
||||
return '昨天';
|
||||
return t('favorites.manager.time.yesterday');
|
||||
} else if (days < 7) {
|
||||
return `${days}天前`;
|
||||
return t('favorites.manager.time.daysAgo', { days });
|
||||
} else {
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
@@ -225,72 +516,54 @@ const formatDate = (timestamp: number) => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.favorite-card {
|
||||
@apply relative cursor-pointer transition-all duration-200;
|
||||
@apply border border-gray-200 dark:border-gray-700;
|
||||
height: 280px;
|
||||
/* 卡片基础样式 */
|
||||
.n-card {
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden; /* 防止内容溢出卡片 */
|
||||
}
|
||||
|
||||
.favorite-card:hover {
|
||||
@apply shadow-lg;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.favorite-card.is-selected {
|
||||
@apply ring-2 ring-blue-500 border-blue-500;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
/* 确保卡片内容区域正确撑满 */
|
||||
.n-card :deep(.n-card__content) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.card-tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-height: 22px;
|
||||
flex-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 操作按钮默认隐藏 */
|
||||
.n-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.is-selected :deep(.n-card) {
|
||||
border-color: rgb(var(--primary-color));
|
||||
box-shadow: 0 0 0 2px rgba(var(--primary-color), 0.2);
|
||||
}
|
||||
|
||||
/* 操作按钮悬停显示 */
|
||||
.card-actions {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.favorite-card:hover .card-actions {
|
||||
.n-card:hover .card-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 公开标识 */
|
||||
.public-badge {
|
||||
@apply absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
@apply bg-yellow-100 dark:bg-yellow-900/50 rounded-full;
|
||||
padding: 4px;
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 移动端优化 */
|
||||
/* 移动端始终显示按钮 */
|
||||
@media (max-width: 768px) {
|
||||
.favorite-card {
|
||||
height: auto;
|
||||
min-height: 240px;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-text {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -2,7 +2,7 @@
|
||||
<n-modal
|
||||
:show="show"
|
||||
preset="card"
|
||||
title="保存到收藏夹"
|
||||
:title="dialogTitle"
|
||||
:style="{ width: 'min(90vw, 1200px)', height: 'min(85vh, 900px)' }"
|
||||
:mask-closable="false"
|
||||
@update:show="handleClose"
|
||||
@@ -10,34 +10,33 @@
|
||||
<n-scrollbar style="max-height: calc(85vh - 150px);">
|
||||
<div style="display: flex; flex-direction: column; gap: 16px;">
|
||||
<!-- 基础信息面板 -->
|
||||
<n-card title="📋 基础信息" :bordered="false" :segmented="{ content: true }" size="small">
|
||||
<n-card :title="t('favorites.dialog.basicInfo')" :bordered="false" :segmented="{ content: true }" size="small">
|
||||
<n-form label-placement="left" :label-width="80">
|
||||
<n-grid :cols="2" :x-gap="16">
|
||||
<!-- 左列 -->
|
||||
<n-grid-item>
|
||||
<n-form-item label="标题" required>
|
||||
<n-form-item :label="t('favorites.dialog.titleLabel')" required>
|
||||
<n-input
|
||||
v-model:value="formData.title"
|
||||
placeholder="为这个提示词起个名字"
|
||||
:placeholder="t('favorites.dialog.titlePlaceholder')"
|
||||
maxlength="100"
|
||||
show-count
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="分类">
|
||||
<n-select
|
||||
v-model:value="formData.category"
|
||||
:options="categoryOptions"
|
||||
placeholder="选择分类(可选)"
|
||||
clearable
|
||||
<n-form-item :label="t('favorites.dialog.categoryLabel')">
|
||||
<CategoryTreeSelect
|
||||
v-model="formData.category"
|
||||
:placeholder="t('favorites.dialog.categoryPlaceholder')"
|
||||
:show-all-option="false"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="功能模式" required>
|
||||
<n-form-item :label="t('favorites.dialog.functionModeLabel')" required>
|
||||
<n-select
|
||||
v-model:value="formData.functionMode"
|
||||
:options="functionModeOptions"
|
||||
:disabled="isFromOptimization"
|
||||
:disabled="props.mode === 'save' && !!props.originalContent"
|
||||
@update:value="handleFunctionModeChange"
|
||||
/>
|
||||
</n-form-item>
|
||||
@@ -45,11 +44,11 @@
|
||||
|
||||
<!-- 右列 -->
|
||||
<n-grid-item>
|
||||
<n-form-item label="描述">
|
||||
<n-form-item :label="t('favorites.dialog.descriptionLabel')">
|
||||
<n-input
|
||||
v-model:value="formData.description"
|
||||
type="textarea"
|
||||
placeholder="描述这个提示词的用途和特点"
|
||||
:placeholder="t('favorites.dialog.descriptionPlaceholder')"
|
||||
:rows="3"
|
||||
maxlength="300"
|
||||
show-count
|
||||
@@ -59,36 +58,36 @@
|
||||
<!-- 动态显示:优化模式或图像模式 -->
|
||||
<n-form-item
|
||||
v-if="formData.functionMode === 'basic' || formData.functionMode === 'context'"
|
||||
label="优化模式"
|
||||
:label="t('favorites.dialog.optimizationModeLabel')"
|
||||
>
|
||||
<n-select
|
||||
v-model:value="formData.optimizationMode"
|
||||
:options="optimizationModeOptions"
|
||||
placeholder="选择优化模式"
|
||||
:disabled="isFromOptimization"
|
||||
:placeholder="t('favorites.dialog.optimizationModePlaceholder')"
|
||||
:disabled="props.mode === 'save' && !!props.originalContent"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item
|
||||
v-if="formData.functionMode === 'image'"
|
||||
label="图像模式"
|
||||
:label="t('favorites.dialog.imageModeLabel')"
|
||||
>
|
||||
<n-select
|
||||
v-model:value="formData.imageSubMode"
|
||||
:options="imageSubModeOptions"
|
||||
placeholder="选择图像模式"
|
||||
:disabled="isFromOptimization"
|
||||
:placeholder="t('favorites.dialog.imageModePlaceholder')"
|
||||
:disabled="props.mode === 'save' && !!props.originalContent"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
|
||||
<!-- 标签(跨越两列) -->
|
||||
<n-form-item label="标签">
|
||||
<n-form-item :label="t('favorites.dialog.tagsLabel')">
|
||||
<n-dynamic-tags
|
||||
v-model:value="formData.tags"
|
||||
:max="10"
|
||||
placeholder="输入标签后按回车添加"
|
||||
:placeholder="t('favorites.dialog.tagsPlaceholder')"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
@@ -97,7 +96,7 @@
|
||||
<!-- 正文内容区域 -->
|
||||
<div>
|
||||
<n-divider style="margin: 0 0 12px 0;">
|
||||
<span style="font-weight: 600;">📝 正文内容</span>
|
||||
<span style="font-weight: 600;">{{ t('favorites.dialog.contentTitle') }}</span>
|
||||
<span style="color: #ff4d4f; margin-left: 4px;">*</span>
|
||||
</n-divider>
|
||||
<OutputDisplayCore
|
||||
@@ -105,7 +104,7 @@
|
||||
mode="editable"
|
||||
:enabled-actions="['copy', 'edit']"
|
||||
height="400px"
|
||||
placeholder="在这里输入提示词内容..."
|
||||
:placeholder="t('favorites.dialog.contentPlaceholder')"
|
||||
@update:content="formData.content = $event"
|
||||
/>
|
||||
</div>
|
||||
@@ -114,9 +113,9 @@
|
||||
|
||||
<template #action>
|
||||
<n-space justify="end">
|
||||
<n-button @click="handleClose" :disabled="saving">取消</n-button>
|
||||
<n-button @click="handleClose" :disabled="saving">{{ t('favorites.dialog.cancel') }}</n-button>
|
||||
<n-button type="primary" :loading="saving" @click="handleSave">
|
||||
保存
|
||||
{{ t('favorites.dialog.save') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
@@ -140,19 +139,39 @@ import {
|
||||
NGrid,
|
||||
NGridItem
|
||||
} from 'naive-ui';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useToast } from '../composables/useToast';
|
||||
import OutputDisplayCore from './OutputDisplayCore.vue';
|
||||
import CategoryTreeSelect from './CategoryTreeSelect.vue';
|
||||
import type { AppServices } from '../types/services';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
interface Props {
|
||||
/** 是否显示对话框 */
|
||||
show: boolean
|
||||
content: string
|
||||
/** 对话框模式: 'create' 新建空白收藏, 'save' 从优化器保存, 'edit' 编辑现有收藏 */
|
||||
mode?: 'create' | 'save' | 'edit'
|
||||
/** 收藏内容(优化后的提示词) */
|
||||
content?: string
|
||||
/** 原始内容(用于从优化器保存时) */
|
||||
originalContent?: string
|
||||
currentFunctionMode: 'basic' | 'context' | 'image'
|
||||
currentOptimizationMode: 'system' | 'user'
|
||||
/** 当前功能模式(用于从优化器保存时预填充) */
|
||||
currentFunctionMode?: 'basic' | 'context' | 'image'
|
||||
/** 当前优化模式(用于从优化器保存时预填充) */
|
||||
currentOptimizationMode?: 'system' | 'user'
|
||||
/** 要编辑的收藏(仅用于 edit 模式) */
|
||||
favorite?: any
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
mode: 'save',
|
||||
content: '',
|
||||
originalContent: undefined,
|
||||
currentFunctionMode: 'basic',
|
||||
currentOptimizationMode: 'system',
|
||||
favorite: undefined
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:show': [value: boolean]
|
||||
@@ -163,7 +182,13 @@ const services = inject<Ref<AppServices | null>>('services');
|
||||
const message = useToast();
|
||||
|
||||
const saving = ref(false);
|
||||
const categories = ref<Array<{ id: string; name: string }>>([]);
|
||||
|
||||
// 对话框标题
|
||||
const dialogTitle = computed(() => {
|
||||
if (props.mode === 'create') return t('favorites.dialog.createTitle');
|
||||
if (props.mode === 'edit') return t('favorites.dialog.editTitle');
|
||||
return t('favorites.dialog.saveTitle');
|
||||
});
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
@@ -177,32 +202,22 @@ const formData = reactive({
|
||||
imageSubMode: undefined as 'text2image' | 'image2image' | undefined
|
||||
});
|
||||
|
||||
// 是否从优化器保存(功能模式字段只读)
|
||||
const isFromOptimization = computed(() => !!props.originalContent);
|
||||
|
||||
// 选项配置
|
||||
const functionModeOptions = [
|
||||
{ label: '基础', value: 'basic' },
|
||||
{ label: '上下文', value: 'context' },
|
||||
{ label: '图像', value: 'image' }
|
||||
];
|
||||
const functionModeOptions = computed(() => [
|
||||
{ label: t('favorites.dialog.functionModes.basic'), value: 'basic' },
|
||||
{ label: t('favorites.dialog.functionModes.context'), value: 'context' },
|
||||
{ label: t('favorites.dialog.functionModes.image'), value: 'image' }
|
||||
]);
|
||||
|
||||
const optimizationModeOptions = [
|
||||
{ label: '系统提示词', value: 'system' },
|
||||
{ label: '用户提示词', value: 'user' }
|
||||
];
|
||||
const optimizationModeOptions = computed(() => [
|
||||
{ label: t('favorites.dialog.optimizationModes.system'), value: 'system' },
|
||||
{ label: t('favorites.dialog.optimizationModes.user'), value: 'user' }
|
||||
]);
|
||||
|
||||
const imageSubModeOptions = [
|
||||
{ label: '文生图', value: 'text2image' },
|
||||
{ label: '图生图', value: 'image2image' }
|
||||
];
|
||||
|
||||
const categoryOptions = computed(() => {
|
||||
return categories.value.map(cat => ({
|
||||
label: cat.name,
|
||||
value: cat.id
|
||||
}));
|
||||
});
|
||||
const imageSubModeOptions = computed(() => [
|
||||
{ label: t('favorites.dialog.imageModes.text2image'), value: 'text2image' },
|
||||
{ label: t('favorites.dialog.imageModes.image2image'), value: 'image2image' }
|
||||
]);
|
||||
|
||||
// 功能模式切换处理
|
||||
const handleFunctionModeChange = (mode: 'basic' | 'context' | 'image') => {
|
||||
@@ -228,106 +243,125 @@ const handleClose = () => {
|
||||
const handleSave = async () => {
|
||||
const servicesValue = services?.value;
|
||||
if (!servicesValue?.favoriteManager) {
|
||||
message.warning('收藏功能暂不可用,请稍后再试');
|
||||
message.warning(t('favorites.dialog.messages.unavailable'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if (!formData.title.trim()) {
|
||||
message.warning('标题不能为空');
|
||||
message.warning(t('favorites.dialog.validation.titleRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.content.trim()) {
|
||||
message.warning('内容不能为空');
|
||||
message.warning(t('favorites.dialog.validation.contentRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
// 构建收藏数据
|
||||
const favoriteData: any = {
|
||||
title: formData.title.trim(),
|
||||
description: formData.description.trim(),
|
||||
content: formData.content.trim(),
|
||||
category: formData.category,
|
||||
tags: formData.tags,
|
||||
functionMode: formData.functionMode,
|
||||
optimizationMode: formData.optimizationMode,
|
||||
imageSubMode: formData.imageSubMode
|
||||
};
|
||||
|
||||
// 如果是从优化器保存,添加元数据
|
||||
if (props.originalContent) {
|
||||
favoriteData.metadata = {
|
||||
originalContent: props.originalContent
|
||||
if (props.mode === 'edit' && props.favorite) {
|
||||
// 编辑模式:更新现有收藏
|
||||
await servicesValue.favoriteManager.updateFavorite(props.favorite.id, {
|
||||
title: formData.title.trim(),
|
||||
description: formData.description.trim(),
|
||||
content: formData.content.trim(),
|
||||
category: formData.category,
|
||||
tags: formData.tags,
|
||||
functionMode: formData.functionMode,
|
||||
optimizationMode: formData.optimizationMode,
|
||||
imageSubMode: formData.imageSubMode
|
||||
});
|
||||
message.success(t('favorites.dialog.messages.editSuccess'));
|
||||
} else {
|
||||
// 创建模式或保存模式:添加新收藏
|
||||
const favoriteData: any = {
|
||||
title: formData.title.trim(),
|
||||
description: formData.description.trim(),
|
||||
content: formData.content.trim(),
|
||||
category: formData.category,
|
||||
tags: formData.tags,
|
||||
functionMode: formData.functionMode,
|
||||
optimizationMode: formData.optimizationMode,
|
||||
imageSubMode: formData.imageSubMode
|
||||
};
|
||||
|
||||
// 如果是从优化器保存,添加元数据
|
||||
if (props.originalContent) {
|
||||
favoriteData.metadata = {
|
||||
originalContent: props.originalContent
|
||||
};
|
||||
}
|
||||
|
||||
await servicesValue.favoriteManager.addFavorite(favoriteData);
|
||||
message.success(t('favorites.dialog.messages.saveSuccess'));
|
||||
}
|
||||
|
||||
await servicesValue.favoriteManager.addFavorite(favoriteData);
|
||||
|
||||
message.success('保存到收藏夹成功');
|
||||
emit('saved');
|
||||
emit('update:show', false);
|
||||
} catch (error: any) {
|
||||
message.error(`保存失败: ${error?.message || '未知错误'}`);
|
||||
const failedKey = props.mode === 'edit' ? 'favorites.dialog.messages.editFailed' : 'favorites.dialog.messages.saveFailed';
|
||||
message.error(`${t(failedKey)}: ${error?.message || '未知错误'}`);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 加载分类列表
|
||||
const loadCategories = async () => {
|
||||
const servicesValue = services?.value;
|
||||
if (!servicesValue?.favoriteManager) return;
|
||||
|
||||
try {
|
||||
const data = await servicesValue.favoriteManager.getCategories();
|
||||
categories.value = data.map(cat => ({
|
||||
id: cat.id,
|
||||
name: cat.name
|
||||
}));
|
||||
} catch (error) {
|
||||
console.warn('加载分类失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听对话框显示,初始化表单
|
||||
watch(() => props.show, (newShow) => {
|
||||
if (newShow) {
|
||||
// 智能预填充
|
||||
// 1. 标题 = 原始提示词前30字符(去除换行符)
|
||||
const titleSource = props.originalContent || props.content;
|
||||
formData.title = titleSource
|
||||
.replace(/\r?\n/g, ' ') // 替换换行为空格
|
||||
.substring(0, 30)
|
||||
.trim();
|
||||
|
||||
// 2. 内容 = 优化后的提示词
|
||||
formData.content = props.content;
|
||||
|
||||
// 3. 根据当前功能模式和优化模式自动设置
|
||||
if (props.currentFunctionMode === 'image') {
|
||||
formData.functionMode = 'image';
|
||||
formData.imageSubMode = 'text2image'; // 默认文生图
|
||||
formData.optimizationMode = undefined;
|
||||
} else if (props.currentFunctionMode === 'context') {
|
||||
formData.functionMode = 'context';
|
||||
formData.optimizationMode = props.currentOptimizationMode;
|
||||
formData.imageSubMode = undefined;
|
||||
} else {
|
||||
if (props.mode === 'create') {
|
||||
// 新建模式: 重置为空表单
|
||||
formData.title = '';
|
||||
formData.description = '';
|
||||
formData.content = '';
|
||||
formData.category = '';
|
||||
formData.tags = [];
|
||||
formData.functionMode = 'basic';
|
||||
formData.optimizationMode = props.currentOptimizationMode;
|
||||
formData.optimizationMode = 'system';
|
||||
formData.imageSubMode = undefined;
|
||||
} else if (props.mode === 'edit' && props.favorite) {
|
||||
// 编辑模式: 加载现有收藏数据
|
||||
formData.title = props.favorite.title;
|
||||
formData.description = props.favorite.description || '';
|
||||
formData.content = props.favorite.content;
|
||||
formData.category = props.favorite.category || '';
|
||||
formData.tags = [...(props.favorite.tags || [])];
|
||||
formData.functionMode = props.favorite.functionMode || 'basic';
|
||||
formData.optimizationMode = props.favorite.optimizationMode;
|
||||
formData.imageSubMode = props.favorite.imageSubMode;
|
||||
} else {
|
||||
// 保存模式: 智能预填充
|
||||
// 1. 标题 = 原始提示词前30字符(去除换行符)
|
||||
const titleSource = props.originalContent || props.content || '';
|
||||
formData.title = titleSource
|
||||
.replace(/\r?\n/g, ' ') // 替换换行为空格
|
||||
.substring(0, 30)
|
||||
.trim();
|
||||
|
||||
// 2. 内容 = 优化后的提示词
|
||||
formData.content = props.content || '';
|
||||
|
||||
// 3. 根据当前功能模式和优化模式自动设置
|
||||
if (props.currentFunctionMode === 'image') {
|
||||
formData.functionMode = 'image';
|
||||
formData.imageSubMode = 'text2image'; // 默认文生图
|
||||
formData.optimizationMode = undefined;
|
||||
} else if (props.currentFunctionMode === 'context') {
|
||||
formData.functionMode = 'context';
|
||||
formData.optimizationMode = props.currentOptimizationMode;
|
||||
formData.imageSubMode = undefined;
|
||||
} else {
|
||||
formData.functionMode = 'basic';
|
||||
formData.optimizationMode = props.currentOptimizationMode;
|
||||
formData.imageSubMode = undefined;
|
||||
}
|
||||
|
||||
// 重置其他字段
|
||||
formData.description = '';
|
||||
formData.category = '';
|
||||
formData.tags = [];
|
||||
}
|
||||
|
||||
// 重置其他字段
|
||||
formData.description = '';
|
||||
formData.category = '';
|
||||
formData.tags = [];
|
||||
|
||||
// 加载分类列表
|
||||
loadCategories();
|
||||
}
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
@@ -27,3 +27,4 @@ export * from './useFocusManager'
|
||||
export * from './useAccessibilityTesting'
|
||||
export * from 'vue-i18n'
|
||||
export * from './useAppInitializer'
|
||||
export * from './useTooltipTheme'
|
||||
@@ -0,0 +1,59 @@
|
||||
import { computed } from 'vue';
|
||||
import type { CSSProperties } from 'vue';
|
||||
import { useThemeVars } from 'naive-ui';
|
||||
import type { TooltipProps } from 'naive-ui';
|
||||
|
||||
type TooltipThemeOverrides = NonNullable<TooltipProps['themeOverrides']>;
|
||||
|
||||
interface UseTooltipThemeOptions {
|
||||
maxWidth?: CSSProperties['maxWidth'];
|
||||
maxHeight?: CSSProperties['maxHeight'];
|
||||
whiteSpace?: CSSProperties['whiteSpace'];
|
||||
wordBreak?: CSSProperties['wordBreak'];
|
||||
overflowWrap?: CSSProperties['overflowWrap'];
|
||||
padding?: CSSProperties['padding'];
|
||||
overflowY?: CSSProperties['overflowY'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Naive UI 默认会将 Tooltip 叠加层合成成接近黑色的背景,这在浅色主题下会显得突兀。
|
||||
* 这里基于 ConfigProvider 的主题变量构建 tooltip 的 themeOverrides,并提供常用的内容样式,
|
||||
* 让 Tooltip 的背景与弹层类组件保持一致,同时限制尺寸防止遮挡。
|
||||
*/
|
||||
export function useTooltipTheme(options: UseTooltipThemeOptions = {}) {
|
||||
const themeVars = useThemeVars();
|
||||
|
||||
const tooltipThemeOverrides = computed<TooltipThemeOverrides>(() => {
|
||||
const vars = themeVars.value;
|
||||
|
||||
return {
|
||||
color: vars.popoverColor,
|
||||
textColor: vars.textColor2,
|
||||
boxShadow: vars.boxShadow2,
|
||||
borderRadius: vars.borderRadius
|
||||
};
|
||||
});
|
||||
|
||||
const tooltipOverlayStyle = computed<CSSProperties>(() => ({
|
||||
maxWidth: options.maxWidth ?? 'calc(100vw - 32px)',
|
||||
maxHeight: options.maxHeight ?? 'calc(100vh - 32px)'
|
||||
}));
|
||||
|
||||
const tooltipContentStyle = computed<CSSProperties>(() => ({
|
||||
maxWidth: '100%',
|
||||
maxHeight: options.maxHeight ?? 'calc(100vh - 32px)',
|
||||
whiteSpace: options.whiteSpace ?? 'pre-wrap',
|
||||
wordBreak: options.wordBreak ?? 'break-word',
|
||||
overflowWrap: options.overflowWrap ?? 'anywhere',
|
||||
padding: options.padding ?? '12px 16px',
|
||||
overflowY: options.overflowY ?? 'auto',
|
||||
border: `1px solid ${themeVars.value.dividerColor}`,
|
||||
boxSizing: 'border-box'
|
||||
}));
|
||||
|
||||
return {
|
||||
tooltipThemeOverrides,
|
||||
tooltipOverlayStyle,
|
||||
tooltipContentStyle
|
||||
};
|
||||
}
|
||||
@@ -103,6 +103,133 @@ export default {
|
||||
},
|
||||
favorites: {
|
||||
title: 'My Favorites',
|
||||
manager: {
|
||||
title: 'Favorites Manager',
|
||||
searchPlaceholder: 'Search favorites...',
|
||||
allCategories: 'All Categories',
|
||||
allTags: 'All Tags',
|
||||
totalCount: '{count} items',
|
||||
import: 'Import',
|
||||
add: 'Add',
|
||||
emptySearchResult: 'No matching favorites found',
|
||||
emptyDescription: 'No favorites yet',
|
||||
startOptimize: 'Start optimizing prompts',
|
||||
importDialog: {
|
||||
title: 'Import Favorites',
|
||||
selectFile: 'Select JSON file',
|
||||
uploadHint: 'Click or drag file to this area',
|
||||
supportFormat: 'Supports .json files',
|
||||
orPasteJson: 'Or paste exported favorites JSON',
|
||||
pastePlaceholder: 'Paste favorites data...',
|
||||
mergeStrategy: 'Merge Strategy',
|
||||
skipDuplicate: 'Skip Duplicates',
|
||||
overwriteDuplicate: 'Overwrite Duplicates',
|
||||
createCopy: 'Create Copy',
|
||||
cancel: 'Cancel',
|
||||
import: 'Import',
|
||||
importing: 'Importing...',
|
||||
selectFileOrPaste: 'Please select a file or paste import data first',
|
||||
importSuccess: 'Import completed: {imported} successful, {skipped} skipped',
|
||||
importPartialFailed: 'Some favorites failed to import',
|
||||
importFailed: 'Import failed',
|
||||
readFileFailed: 'Failed to read file'
|
||||
},
|
||||
categoryManager: {
|
||||
title: 'Category Manager'
|
||||
},
|
||||
actions: {
|
||||
export: 'Export Favorites',
|
||||
clear: 'Clear Favorites',
|
||||
exportSuccess: 'Export successful',
|
||||
exportFailed: 'Export failed',
|
||||
clearConfirm: 'Are you sure you want to clear all favorites? This action cannot be undone.',
|
||||
clearSuccess: 'Cleared successfully',
|
||||
clearFailed: 'Clear failed',
|
||||
deleteConfirm: 'Are you sure you want to delete favorite "{title}"? This action cannot be undone.',
|
||||
deleteSuccess: 'Deleted successfully',
|
||||
deleteFailed: 'Delete failed',
|
||||
copySuccess: 'Copied to clipboard',
|
||||
copyFailed: 'Copy failed',
|
||||
copiedOptimized: 'Optimized prompt copied',
|
||||
copiedReasoning: 'Reasoning content copied',
|
||||
copiedAll: 'Content copied',
|
||||
shareComingSoon: 'Share feature coming soon',
|
||||
categoryManagementComingSoon: 'Category management coming in a future version'
|
||||
},
|
||||
preview: {
|
||||
title: 'Favorite Details',
|
||||
updatedAt: 'Updated {time}'
|
||||
},
|
||||
messages: {
|
||||
unavailable: 'Favorites feature is temporarily unavailable, please try again later',
|
||||
loadFailed: 'Failed to load favorites',
|
||||
loadCategoryFailed: 'Failed to load categories',
|
||||
managerNotInitialized: 'Favorites manager not initialized'
|
||||
},
|
||||
time: {
|
||||
justNow: 'Just now',
|
||||
minutesAgo: '{minutes} minutes ago',
|
||||
hoursAgo: '{hours} hours ago',
|
||||
yesterday: 'Yesterday',
|
||||
daysAgo: '{days} days ago'
|
||||
},
|
||||
card: {
|
||||
useCount: 'Use count',
|
||||
copyContent: 'Copy content',
|
||||
useNow: 'Use now',
|
||||
edit: 'Edit',
|
||||
delete: 'Delete',
|
||||
cancel: 'Cancel',
|
||||
deleteConfirm: 'Delete "{title}"?'
|
||||
}
|
||||
},
|
||||
dialog: {
|
||||
createTitle: 'Create Favorite',
|
||||
editTitle: 'Edit Favorite',
|
||||
saveTitle: 'Save to Favorites',
|
||||
basicInfo: '📋 Basic Information',
|
||||
contentTitle: '📝 Content',
|
||||
titleLabel: 'Title',
|
||||
titlePlaceholder: 'Name this prompt',
|
||||
categoryLabel: 'Category',
|
||||
categoryPlaceholder: 'Select category (optional)',
|
||||
functionModeLabel: 'Function Mode',
|
||||
descriptionLabel: 'Description',
|
||||
descriptionPlaceholder: 'Describe the purpose and characteristics of this prompt',
|
||||
optimizationModeLabel: 'Optimization Mode',
|
||||
optimizationModePlaceholder: 'Select optimization mode',
|
||||
imageModeLabel: 'Image Mode',
|
||||
imageModePlaceholder: 'Select image mode',
|
||||
tagsLabel: 'Tags',
|
||||
tagsPlaceholder: 'Enter tags and press Enter to add',
|
||||
contentPlaceholder: 'Enter prompt content here...',
|
||||
cancel: 'Cancel',
|
||||
save: 'Save',
|
||||
functionModes: {
|
||||
basic: 'Basic',
|
||||
context: 'Context',
|
||||
image: 'Image'
|
||||
},
|
||||
optimizationModes: {
|
||||
system: 'System Prompt',
|
||||
user: 'User Prompt'
|
||||
},
|
||||
imageModes: {
|
||||
text2image: 'Text-to-Image',
|
||||
image2image: 'Image-to-Image'
|
||||
},
|
||||
validation: {
|
||||
titleRequired: 'Title is required',
|
||||
contentRequired: 'Content is required'
|
||||
},
|
||||
messages: {
|
||||
editSuccess: 'Edit successful',
|
||||
saveSuccess: 'Saved to favorites successfully',
|
||||
saveFailed: 'Save failed',
|
||||
editFailed: 'Edit failed',
|
||||
unavailable: 'Favorites feature is temporarily unavailable, please try again later'
|
||||
}
|
||||
},
|
||||
categoryManager: {
|
||||
title: 'Category Manager',
|
||||
addRootCategory: 'Add Root Category',
|
||||
@@ -134,6 +261,20 @@ export default {
|
||||
validation: {
|
||||
nameRequired: 'Please enter category name',
|
||||
nameLength: 'Category name length should be 1-50 characters'
|
||||
},
|
||||
defaultCategories: {
|
||||
uncategorized: 'Uncategorized',
|
||||
uncategorizedDesc: 'Default category',
|
||||
systemPrompt: 'System Prompts',
|
||||
systemPromptDesc: 'Optimized system prompts',
|
||||
userPrompt: 'User Prompts',
|
||||
userPromptDesc: 'Optimized user prompts',
|
||||
creativeWriting: 'Creative Writing',
|
||||
creativeWritingDesc: 'Creative writing related prompts',
|
||||
programming: 'Programming',
|
||||
programmingDesc: 'Programming and development related prompts',
|
||||
businessAnalysis: 'Business Analysis',
|
||||
businessAnalysisDesc: 'Business analysis related prompts'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -103,6 +103,133 @@
|
||||
},
|
||||
favorites: {
|
||||
title: '我的收藏',
|
||||
manager: {
|
||||
title: '收藏管理',
|
||||
searchPlaceholder: '搜索收藏...',
|
||||
allCategories: '全部分类',
|
||||
allTags: '全部标签',
|
||||
totalCount: '共 {count} 项',
|
||||
import: '导入',
|
||||
add: '添加',
|
||||
emptySearchResult: '没有找到匹配的收藏',
|
||||
emptyDescription: '还没有收藏任何提示词',
|
||||
startOptimize: '开始优化提示词',
|
||||
importDialog: {
|
||||
title: '导入收藏',
|
||||
selectFile: '选择 JSON 文件',
|
||||
uploadHint: '点击或拖拽文件到此区域',
|
||||
supportFormat: '支持 .json 文件',
|
||||
orPasteJson: '或粘贴导出的收藏 JSON',
|
||||
pastePlaceholder: '粘贴收藏数据...',
|
||||
mergeStrategy: '合并策略',
|
||||
skipDuplicate: '跳过重复',
|
||||
overwriteDuplicate: '覆盖重复',
|
||||
createCopy: '创建副本',
|
||||
cancel: '取消',
|
||||
import: '导入',
|
||||
importing: '导入中...',
|
||||
selectFileOrPaste: '请先选择文件或粘贴导入数据',
|
||||
importSuccess: '导入完成:成功 {imported} 项,跳过 {skipped} 项',
|
||||
importPartialFailed: '部分收藏导入失败',
|
||||
importFailed: '导入失败',
|
||||
readFileFailed: '读取文件失败'
|
||||
},
|
||||
categoryManager: {
|
||||
title: '分类管理'
|
||||
},
|
||||
actions: {
|
||||
export: '导出收藏',
|
||||
clear: '清空收藏',
|
||||
exportSuccess: '导出成功',
|
||||
exportFailed: '导出失败',
|
||||
clearConfirm: '确定要清空所有收藏吗?此操作不可恢复。',
|
||||
clearSuccess: '清空成功',
|
||||
clearFailed: '清空失败',
|
||||
deleteConfirm: '确定要删除收藏 "{title}" 吗?此操作不可恢复。',
|
||||
deleteSuccess: '删除成功',
|
||||
deleteFailed: '删除失败',
|
||||
copySuccess: '已复制到剪贴板',
|
||||
copyFailed: '复制失败',
|
||||
copiedOptimized: '已复制优化后的提示词',
|
||||
copiedReasoning: '已复制推理内容',
|
||||
copiedAll: '已复制内容',
|
||||
shareComingSoon: '分享功能即将上线',
|
||||
categoryManagementComingSoon: '分类管理功能将在后续版本提供'
|
||||
},
|
||||
preview: {
|
||||
title: '收藏详情',
|
||||
updatedAt: '更新于 {time}'
|
||||
},
|
||||
messages: {
|
||||
unavailable: '收藏功能暂不可用,请稍后再试',
|
||||
loadFailed: '加载收藏失败',
|
||||
loadCategoryFailed: '加载分类失败',
|
||||
managerNotInitialized: '收藏管理器未初始化'
|
||||
},
|
||||
time: {
|
||||
justNow: '刚刚',
|
||||
minutesAgo: '{minutes}分钟前',
|
||||
hoursAgo: '{hours}小时前',
|
||||
yesterday: '昨天',
|
||||
daysAgo: '{days}天前'
|
||||
},
|
||||
card: {
|
||||
useCount: '使用次数',
|
||||
copyContent: '复制内容',
|
||||
useNow: '立即使用',
|
||||
edit: '编辑',
|
||||
delete: '删除',
|
||||
cancel: '取消',
|
||||
deleteConfirm: '确定删除"{title}"吗?'
|
||||
}
|
||||
},
|
||||
dialog: {
|
||||
createTitle: '新建收藏',
|
||||
editTitle: '编辑收藏',
|
||||
saveTitle: '保存到收藏夹',
|
||||
basicInfo: '📋 基础信息',
|
||||
contentTitle: '📝 正文内容',
|
||||
titleLabel: '标题',
|
||||
titlePlaceholder: '为这个提示词起个名字',
|
||||
categoryLabel: '分类',
|
||||
categoryPlaceholder: '选择分类(可选)',
|
||||
functionModeLabel: '功能模式',
|
||||
descriptionLabel: '描述',
|
||||
descriptionPlaceholder: '描述这个提示词的用途和特点',
|
||||
optimizationModeLabel: '优化模式',
|
||||
optimizationModePlaceholder: '选择优化模式',
|
||||
imageModeLabel: '图像模式',
|
||||
imageModePlaceholder: '选择图像模式',
|
||||
tagsLabel: '标签',
|
||||
tagsPlaceholder: '输入标签后按回车添加',
|
||||
contentPlaceholder: '在这里输入提示词内容...',
|
||||
cancel: '取消',
|
||||
save: '保存',
|
||||
functionModes: {
|
||||
basic: '基础',
|
||||
context: '上下文',
|
||||
image: '图像'
|
||||
},
|
||||
optimizationModes: {
|
||||
system: '系统提示词',
|
||||
user: '用户提示词'
|
||||
},
|
||||
imageModes: {
|
||||
text2image: '文生图',
|
||||
image2image: '图生图'
|
||||
},
|
||||
validation: {
|
||||
titleRequired: '标题不能为空',
|
||||
contentRequired: '内容不能为空'
|
||||
},
|
||||
messages: {
|
||||
editSuccess: '编辑成功',
|
||||
saveSuccess: '保存到收藏夹成功',
|
||||
saveFailed: '保存失败',
|
||||
editFailed: '编辑失败',
|
||||
unavailable: '收藏功能暂不可用,请稍后再试'
|
||||
}
|
||||
},
|
||||
categoryManager: {
|
||||
title: '分类管理',
|
||||
addRootCategory: '添加根分类',
|
||||
@@ -134,6 +261,20 @@
|
||||
validation: {
|
||||
nameRequired: '请输入分类名称',
|
||||
nameLength: '分类名称长度应在 1-50 之间'
|
||||
},
|
||||
defaultCategories: {
|
||||
uncategorized: '未分类',
|
||||
uncategorizedDesc: '默认分类',
|
||||
systemPrompt: '系统提示词',
|
||||
systemPromptDesc: '优化后的系统提示词',
|
||||
userPrompt: '用户提示词',
|
||||
userPromptDesc: '优化后的用户提示词',
|
||||
creativeWriting: '创意写作',
|
||||
creativeWritingDesc: '创意写作相关的提示词',
|
||||
programming: '编程开发',
|
||||
programmingDesc: '编程开发相关的提示词',
|
||||
businessAnalysis: '商业分析',
|
||||
businessAnalysisDesc: '商业分析相关的提示词'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -103,6 +103,180 @@ export default {
|
||||
},
|
||||
favorites: {
|
||||
title: '我的收藏',
|
||||
manager: {
|
||||
title: '收藏管理',
|
||||
searchPlaceholder: '搜尋收藏...',
|
||||
allCategories: '全部分類',
|
||||
allTags: '全部標籤',
|
||||
totalCount: '共 {count} 項',
|
||||
import: '匯入',
|
||||
add: '新增',
|
||||
emptySearchResult: '沒有找到相符的收藏',
|
||||
emptyDescription: '還沒有收藏任何提示詞',
|
||||
startOptimize: '開始優化提示詞',
|
||||
importDialog: {
|
||||
title: '匯入收藏',
|
||||
selectFile: '選擇 JSON 檔案',
|
||||
uploadHint: '點選或拖曳檔案到此區域',
|
||||
supportFormat: '支援 .json 檔案',
|
||||
orPasteJson: '或貼上匯出的收藏 JSON',
|
||||
pastePlaceholder: '貼上收藏資料...',
|
||||
mergeStrategy: '合併策略',
|
||||
skipDuplicate: '跳過重複',
|
||||
overwriteDuplicate: '覆蓋重複',
|
||||
createCopy: '建立副本',
|
||||
cancel: '取消',
|
||||
import: '匯入',
|
||||
importing: '匯入中...',
|
||||
selectFileOrPaste: '請先選擇檔案或貼上匯入資料',
|
||||
importSuccess: '匯入完成:成功 {imported} 項,跳過 {skipped} 項',
|
||||
importPartialFailed: '部分收藏匯入失敗',
|
||||
importFailed: '匯入失敗',
|
||||
readFileFailed: '讀取檔案失敗'
|
||||
},
|
||||
categoryManager: {
|
||||
title: '分類管理'
|
||||
},
|
||||
actions: {
|
||||
export: '匯出收藏',
|
||||
clear: '清空收藏',
|
||||
exportSuccess: '匯出成功',
|
||||
exportFailed: '匯出失敗',
|
||||
clearConfirm: '確定要清空所有收藏嗎?此操作不可恢復。',
|
||||
clearSuccess: '清空成功',
|
||||
clearFailed: '清空失敗',
|
||||
deleteConfirm: '確定要刪除收藏 "{title}" 嗎?此操作不可恢復。',
|
||||
deleteSuccess: '刪除成功',
|
||||
deleteFailed: '刪除失敗',
|
||||
copySuccess: '已複製到剪貼簿',
|
||||
copyFailed: '複製失敗',
|
||||
copiedOptimized: '已複製優化後的提示詞',
|
||||
copiedReasoning: '已複製推理內容',
|
||||
copiedAll: '已複製內容',
|
||||
shareComingSoon: '分享功能即將上線',
|
||||
categoryManagementComingSoon: '分類管理功能將在後續版本提供'
|
||||
},
|
||||
preview: {
|
||||
title: '收藏詳情',
|
||||
updatedAt: '更新於 {time}'
|
||||
},
|
||||
messages: {
|
||||
unavailable: '收藏功能暫不可用,請稍後再試',
|
||||
loadFailed: '載入收藏失敗',
|
||||
loadCategoryFailed: '載入分類失敗',
|
||||
managerNotInitialized: '收藏管理器未初始化'
|
||||
},
|
||||
time: {
|
||||
justNow: '剛剛',
|
||||
minutesAgo: '{minutes}分鐘前',
|
||||
hoursAgo: '{hours}小時前',
|
||||
yesterday: '昨天',
|
||||
daysAgo: '{days}天前'
|
||||
},
|
||||
card: {
|
||||
useCount: '使用次數',
|
||||
copyContent: '複製內容',
|
||||
useNow: '立即使用',
|
||||
edit: '編輯',
|
||||
delete: '刪除',
|
||||
cancel: '取消',
|
||||
deleteConfirm: '確定刪除"{title}"嗎?'
|
||||
}
|
||||
},
|
||||
dialog: {
|
||||
createTitle: '新建收藏',
|
||||
editTitle: '編輯收藏',
|
||||
saveTitle: '儲存到收藏夾',
|
||||
basicInfo: '📋 基礎資訊',
|
||||
contentTitle: '📝 正文內容',
|
||||
titleLabel: '標題',
|
||||
titlePlaceholder: '為這個提示詞起個名字',
|
||||
categoryLabel: '分類',
|
||||
categoryPlaceholder: '選擇分類(可選)',
|
||||
functionModeLabel: '功能模式',
|
||||
descriptionLabel: '描述',
|
||||
descriptionPlaceholder: '描述這個提示詞的用途和特點',
|
||||
optimizationModeLabel: '優化模式',
|
||||
optimizationModePlaceholder: '選擇優化模式',
|
||||
imageModeLabel: '圖像模式',
|
||||
imageModePlaceholder: '選擇圖像模式',
|
||||
tagsLabel: '標籤',
|
||||
tagsPlaceholder: '輸入標籤後按Enter新增',
|
||||
contentPlaceholder: '在這裡輸入提示詞內容...',
|
||||
cancel: '取消',
|
||||
save: '儲存',
|
||||
functionModes: {
|
||||
basic: '基礎',
|
||||
context: '情境',
|
||||
image: '圖像'
|
||||
},
|
||||
optimizationModes: {
|
||||
system: '系統提示詞',
|
||||
user: '使用者提示詞'
|
||||
},
|
||||
imageModes: {
|
||||
text2image: '文生圖',
|
||||
image2image: '圖生圖'
|
||||
},
|
||||
validation: {
|
||||
titleRequired: '標題不能為空',
|
||||
contentRequired: '內容不能為空'
|
||||
},
|
||||
messages: {
|
||||
editSuccess: '編輯成功',
|
||||
saveSuccess: '儲存到收藏夾成功',
|
||||
saveFailed: '儲存失敗',
|
||||
editFailed: '編輯失敗',
|
||||
unavailable: '收藏功能暫不可用,請稍後再試'
|
||||
}
|
||||
},
|
||||
categoryManager: {
|
||||
title: '分類管理',
|
||||
addRootCategory: '新增根分類',
|
||||
addSubCategory: '新增子分類',
|
||||
expandAll: '全部展開',
|
||||
collapseAll: '全部摺疊',
|
||||
edit: '編輯',
|
||||
delete: '刪除',
|
||||
editCategory: '編輯分類',
|
||||
addCategory: '新增分類',
|
||||
categoryName: '分類名稱',
|
||||
categoryNamePlaceholder: '請輸入分類名稱',
|
||||
categoryDescription: '分類描述',
|
||||
categoryDescriptionPlaceholder: '請輸入分類描述',
|
||||
parentCategory: '父分類',
|
||||
parentCategoryPlaceholder: '選擇父分類(不選則為根分類)',
|
||||
categoryColor: '分類顏色',
|
||||
confirmDelete: '確認刪除',
|
||||
deleteWarning: '確定要刪除分類 <strong>{name}</strong> 嗎?',
|
||||
deleteChildrenWarning: '警告:該分類下還有 {count} 個子分類,將一併刪除!',
|
||||
deleteUsageWarning: '提示:該分類已被 {count} 個收藏使用,刪除後這些收藏將變為未分類。',
|
||||
deleteSuccess: '刪除成功',
|
||||
deleteFailed: '刪除失敗',
|
||||
saveSuccess: '儲存成功',
|
||||
addSuccess: '新增成功',
|
||||
updateSuccess: '更新成功',
|
||||
saveFailed: '儲存失敗',
|
||||
loadFailed: '載入分類失敗',
|
||||
validation: {
|
||||
nameRequired: '請輸入分類名稱',
|
||||
nameLength: '分類名稱長度應在 1-50 之間'
|
||||
},
|
||||
defaultCategories: {
|
||||
uncategorized: '未分類',
|
||||
uncategorizedDesc: '預設分類',
|
||||
systemPrompt: '系統提示詞',
|
||||
systemPromptDesc: '優化後的系統提示詞',
|
||||
userPrompt: '使用者提示詞',
|
||||
userPromptDesc: '優化後的使用者提示詞',
|
||||
creativeWriting: '創意寫作',
|
||||
creativeWritingDesc: '創意寫作相關的提示詞',
|
||||
programming: '程式開發',
|
||||
programmingDesc: '程式開發相關的提示詞',
|
||||
businessAnalysis: '商業分析',
|
||||
businessAnalysisDesc: '商業分析相關的提示詞'
|
||||
}
|
||||
}
|
||||
},
|
||||
promptOptimizer: {
|
||||
title: '提示詞優化器',
|
||||
|
||||
Reference in new issue
Block a user