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:
linshen committed 2025-10-18 21:43:53 +08:00
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>
+426 -153
View File
@@ -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
+157 -123
View File
@@ -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>
+1
View File
@@ -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
};
}
+141
View File
@@ -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'
}
}
},
+141
View File
@@ -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: '商业分析相关的提示词'
}
}
},
+174
View File
@@ -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: '提示詞優化器',