feat(favorite, ui): 完善收藏功能,引入独立标签管理、国际化支持及全面集成测试
本次更新全面增强收藏功能,引入独立的标签管理系统,完善了多语言支持,并对UI组件进行了优化。同时,新增了大量集成测试以确保功能的稳定性和数据完整性。
## 新增功能与优化
### 1. 独立标签管理系统
- **核心功能**:
- **独立标签库 (STORAGE_KEYS.TAGS)**: 实现标签的独立存储,不依赖收藏项,支持预先创建使用次数为0的标签。
- **标签CRUD操作**: 提供 `addTag()` (支持重复检测)、`renameTag()` (更新所有相关收藏和独立库)、`mergeTags()` (合并多个标签)、`deleteTag()` (从独立库和收藏中移除) 功能。
- **标签统计算法**: `getAllTags()` 方法返回独立标签库与收藏中使用标签的合集,并支持按使用次数降序排序。
- **导入导出优化**: 导出内容包含独立标签库,导入时自动重建,确保数据完整性。
- **类型转换工具 (TagTypeConverter)**: 解决API与UI格式不一致问题,提供 `toTagStatistics()`、`toAutoCompleteOptions()` 等转换方法,并支持多种排序。
- **错误处理增强**: 新增 `FavoriteTagError` 基类及 `FavoriteTagAlreadyExistsError`、`FavoriteTagNotFoundError` 等专用错误类型。
### 2. 标签管理器UI组件 (TagManager.vue)
- 提供完整的标签CRUD界面,包括数据表格展示、实时搜索、新增、重命名、合并、删除、分页功能。
- 技术栈:Naive UI (NDataTable, NModal, NPopconfirm),使用 `TagTypeConverter` 统一数据格式。
- 集成点:FavoriteManager.vue 中新增“标签管理”按钮以打开该对话框。
### 3. 智能标签建议 (useTagSuggestions)
- **智能自动完成**: 基于现有标签使用统计提供实时建议,热门标签优先,支持前缀匹配,并自动去重过滤。
- **API**: 提供 `loadTags()`、`filterTags()`、`getPopularTags()`、`getRecentTags()` 方法。
- 集成点:SaveFavoriteDialog.vue 中集成标签自动完成输入框。
### 4. 收藏功能国际化与UI优化
- **国际化覆盖 (中英繁)**:
- 新增 `favorites.dialog.*` (保存对话框)、`favorites.manager.*` (收藏管理器)、`favorites.manager.tagManager.*` (标签管理器)、`favorites.card.*` (收藏卡片) 等完整翻译键。
- 遵循翻译规范:使用全角标点、统一术语、简洁明了的提示信息。
- **UI组件优化**:
- **SaveFavoriteDialog.vue**:
- **标签自动完成集成**: 使用 `useTagSuggestions` 提供智能建议,支持回车添加,已选标签显示为可关闭 `Tag`,保存前自动注册标签到独立库。
- **智能预填充**: 根据模式(创建/保存/编辑)智能预填充标题和内容。
- **FavoriteManager.vue**:
- 新增“标签管理”按钮。
- 网格布局优化 (使用 `NGrid` 替代 `flex`)。
- 工具栏固定,响应式设计。
- 移除冗余视图切换,简化布局。
- **FavoriteCard.vue**:
- 卡片布局优化:使用 `NTag` 展示标签,显示元信息 (创建时间、使用次数、功能模式),操作按钮 (编辑、删除、应用),悬停效果。
- **用户体验提升**: 优化标签输入和管理流程,增强错误处理(如 `TAG_ALREADY_EXISTS` 静默忽略,其他错误显示友好提示)。
### 5. 分类管理架构优化与国际化
- **架构优化**:
- 分离关注点:默认分类创建从 Core 层移至 UI 层,Core 层不再硬编码分类名称,改为接受配置参数。
- 添加初始化标记防止重复创建默认分类。
- `deleteCategory` 现返回受影响的收藏数量。
- **国际化增强**:
- 新增 `useFavoriteInitializer` composable 管理国际化分类。
- 完善中英繁三语言的分类翻译。
- 优化默认分类配置 (新增“学习研究”和“日常助手”)。
- 统一错误信息为英文。
- **代码组织**:
- 测试文件迁移至标准目录 `packages/core/tests/`。
- 改进 `FavoriteManager` 组件生命周期管理,优化初始化流程。
### 6. 全面集成测试
- 新增 `integration.test.ts` (742行,20个集成测试),覆盖以下场景:
- CRUD 完整流程测试 (3个)。
- 历史记录转收藏测试 (5个,覆盖12种 PromptRecordType)。
- 标签和分类管理测试 (6个:重命名/合并/删除/排序/统计/级联)。
- 导入导出完整性测试 (2个)。
- 搜索和过滤功能测试 (4个)。
- `type-converter.test.ts` 包含16个测试,`tag-manager.test.ts` 包含23个测试。
This commit is contained in:
1 parent
5bf02fa4df
commit
d784c99d88
21 files changed
+3908
-164
No files matched your search
@@ -147,6 +147,7 @@ export * from './services/context/constants'
|
||||
// 导出收藏管理相关
|
||||
export { FavoriteManager } from './services/favorite/manager'
|
||||
export { FavoriteManagerElectronProxy } from './services/favorite/electron-proxy'
|
||||
export { TagTypeConverter } from './services/favorite/type-converter'
|
||||
export * from './services/favorite/types'
|
||||
export * from './services/favorite/errors'
|
||||
|
||||
|
||||
@@ -5,7 +5,12 @@ import {
|
||||
FavoriteAlreadyExistsError,
|
||||
FavoriteCategoryNotFoundError,
|
||||
FavoriteValidationError,
|
||||
FavoriteStorageError
|
||||
FavoriteStorageError,
|
||||
FavoriteTagAlreadyExistsError,
|
||||
FavoriteTagNotFoundError,
|
||||
FavoriteTagError,
|
||||
FavoriteMigrationError,
|
||||
FavoriteImportExportError
|
||||
} from './errors';
|
||||
|
||||
declare const window: {
|
||||
@@ -48,6 +53,23 @@ export class FavoriteManagerElectronProxy implements IFavoriteManager {
|
||||
if (error.code === 'STORAGE_ERROR') {
|
||||
throw new FavoriteStorageError(error.message || '');
|
||||
}
|
||||
// 标签相关错误
|
||||
if (error.code === 'TAG_ALREADY_EXISTS') {
|
||||
throw new FavoriteTagAlreadyExistsError(error.tag || '');
|
||||
}
|
||||
if (error.code === 'TAG_NOT_FOUND') {
|
||||
throw new FavoriteTagNotFoundError(error.tag || '');
|
||||
}
|
||||
if (error.code === 'TAG_ERROR') {
|
||||
throw new FavoriteTagError(error.message || '', error.code);
|
||||
}
|
||||
// 数据迁移和导入导出错误
|
||||
if (error.code === 'MIGRATION_ERROR') {
|
||||
throw new FavoriteMigrationError(error.message || '', error.cause);
|
||||
}
|
||||
if (error.code === 'IMPORT_EXPORT_ERROR') {
|
||||
throw new FavoriteImportExportError(error.message || '', error.cause, error.details);
|
||||
}
|
||||
throw new FavoriteError(error.message || '未知错误');
|
||||
}
|
||||
}
|
||||
@@ -100,7 +122,7 @@ export class FavoriteManagerElectronProxy implements IFavoriteManager {
|
||||
return this.invokeMethod('updateCategory', id, updates);
|
||||
}
|
||||
|
||||
async deleteCategory(id: string): Promise<void> {
|
||||
async deleteCategory(id: string): Promise<number> {
|
||||
return this.invokeMethod('deleteCategory', id);
|
||||
}
|
||||
|
||||
@@ -134,6 +156,10 @@ export class FavoriteManagerElectronProxy implements IFavoriteManager {
|
||||
return this.invokeMethod('getAllTags');
|
||||
}
|
||||
|
||||
async addTag(tag: string): Promise<void> {
|
||||
return this.invokeMethod('addTag', tag);
|
||||
}
|
||||
|
||||
async renameTag(oldTag: string, newTag: string): Promise<number> {
|
||||
return this.invokeMethod('renameTag', oldTag, newTag);
|
||||
}
|
||||
|
||||
@@ -11,35 +11,85 @@ export class FavoriteError extends Error {
|
||||
|
||||
export class FavoriteNotFoundError extends FavoriteError {
|
||||
constructor(id: string) {
|
||||
super(`收藏项未找到: ${id}`, 'FAVORITE_NOT_FOUND');
|
||||
super(`Favorite not found: ${id}`, 'FAVORITE_NOT_FOUND');
|
||||
this.name = 'FavoriteNotFoundError';
|
||||
}
|
||||
}
|
||||
|
||||
export class FavoriteAlreadyExistsError extends FavoriteError {
|
||||
constructor(content: string) {
|
||||
super(`收藏项已存在: ${content.slice(0, 50)}...`, 'FAVORITE_ALREADY_EXISTS');
|
||||
super(`Favorite already exists: ${content.slice(0, 50)}...`, 'FAVORITE_ALREADY_EXISTS');
|
||||
this.name = 'FavoriteAlreadyExistsError';
|
||||
}
|
||||
}
|
||||
|
||||
export class FavoriteCategoryNotFoundError extends FavoriteError {
|
||||
constructor(id: string) {
|
||||
super(`分类未找到: ${id}`, 'CATEGORY_NOT_FOUND');
|
||||
super(`Category not found: ${id}`, 'CATEGORY_NOT_FOUND');
|
||||
this.name = 'FavoriteCategoryNotFoundError';
|
||||
}
|
||||
}
|
||||
|
||||
export class FavoriteValidationError extends FavoriteError {
|
||||
constructor(message: string) {
|
||||
super(`验证错误: ${message}`, 'VALIDATION_ERROR');
|
||||
super(`Validation error: ${message}`, 'VALIDATION_ERROR');
|
||||
this.name = 'FavoriteValidationError';
|
||||
}
|
||||
}
|
||||
|
||||
export class FavoriteStorageError extends FavoriteError {
|
||||
constructor(message: string) {
|
||||
super(`存储错误: ${message}`, 'STORAGE_ERROR');
|
||||
constructor(message: string, public cause?: Error) {
|
||||
super(`Storage error: ${message}`, 'STORAGE_ERROR');
|
||||
this.name = 'FavoriteStorageError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签相关错误
|
||||
*/
|
||||
export class FavoriteTagError extends FavoriteError {
|
||||
constructor(message: string, code: string = 'TAG_ERROR') {
|
||||
super(message, code);
|
||||
this.name = 'FavoriteTagError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签已存在错误
|
||||
*/
|
||||
export class FavoriteTagAlreadyExistsError extends FavoriteTagError {
|
||||
constructor(tag: string) {
|
||||
super(`Tag already exists: ${tag}`, 'TAG_ALREADY_EXISTS');
|
||||
this.name = 'FavoriteTagAlreadyExistsError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag not found error
|
||||
*/
|
||||
export class FavoriteTagNotFoundError extends FavoriteTagError {
|
||||
constructor(tag: string) {
|
||||
super(`Tag not found: ${tag}`, 'TAG_NOT_FOUND');
|
||||
this.name = 'FavoriteTagNotFoundError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data migration error
|
||||
*/
|
||||
export class FavoriteMigrationError extends FavoriteError {
|
||||
constructor(message: string, public cause?: Error) {
|
||||
super(`Migration error: ${message}`, 'MIGRATION_ERROR');
|
||||
this.name = 'FavoriteMigrationError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入导出错误
|
||||
*/
|
||||
export class FavoriteImportExportError extends FavoriteError {
|
||||
constructor(message: string, public cause?: Error, public details?: string[]) {
|
||||
super(message, 'IMPORT_EXPORT_ERROR');
|
||||
this.name = 'FavoriteImportExportError';
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,12 @@
|
||||
export * from './types';
|
||||
export * from './errors';
|
||||
export * from './manager';
|
||||
export type { FavoritePrompt, FavoriteCategory, FavoriteStats, IFavoriteManager } from './types';
|
||||
export * from './type-converter';
|
||||
export type {
|
||||
FavoritePrompt,
|
||||
FavoriteCategory,
|
||||
FavoriteStats,
|
||||
FavoriteTag,
|
||||
TagStatistics,
|
||||
IFavoriteManager
|
||||
} from './types';
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
FavoritePrompt,
|
||||
FavoriteCategory,
|
||||
FavoriteStats,
|
||||
FavoriteTag,
|
||||
IFavoriteManager
|
||||
} from './types';
|
||||
import {
|
||||
@@ -10,7 +11,10 @@ import {
|
||||
FavoriteNotFoundError,
|
||||
FavoriteCategoryNotFoundError,
|
||||
FavoriteValidationError,
|
||||
FavoriteStorageError
|
||||
FavoriteStorageError,
|
||||
FavoriteTagAlreadyExistsError,
|
||||
FavoriteMigrationError,
|
||||
FavoriteImportExportError
|
||||
} from './errors';
|
||||
import { TypeMapper } from './type-mapper';
|
||||
|
||||
@@ -21,7 +25,8 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
private readonly STORAGE_KEYS = {
|
||||
FAVORITES: 'favorites',
|
||||
CATEGORIES: 'favorite_categories',
|
||||
STATS: 'favorite_stats'
|
||||
STATS: 'favorite_stats',
|
||||
TAGS: 'favorite_tags'
|
||||
} as const;
|
||||
|
||||
private initPromise: Promise<void>;
|
||||
@@ -48,7 +53,8 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
|
||||
try {
|
||||
this.initState = 'initializing';
|
||||
await this.initializeDefaultCategories();
|
||||
// ❌ 移除自动创建默认分类 - 改由 UI 层调用 ensureDefaultCategories
|
||||
// await this.initializeDefaultCategories();
|
||||
await this.migrateLegacyData();
|
||||
this.initialized = true;
|
||||
this.initState = 'initialized';
|
||||
@@ -119,91 +125,61 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
if (migrated) {
|
||||
// 迁移后更新统计信息
|
||||
await this.updateStats();
|
||||
console.info('[FavoriteManager] 数据迁移完成,已更新收藏项格式');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[FavoriteManager] 旧数据迁移失败:', error);
|
||||
// 迁移失败不应该阻止服务初始化
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const migrationError = new FavoriteMigrationError(
|
||||
`Legacy data migration failed: ${errorMessage}`,
|
||||
error instanceof Error ? error : undefined
|
||||
);
|
||||
console.warn('[FavoriteManager]', migrationError);
|
||||
// 迁移失败不应该阻止服务初始化,仅记录警告
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化默认分类
|
||||
* 确保默认分类存在(仅首次)
|
||||
* 由 UI 层调用,传入国际化后的分类配置
|
||||
*
|
||||
* 💡 **架构说明**:
|
||||
* 此方法在初始化期间被调用,现在可以安全地调用公共方法。
|
||||
* `ensureInitialized()` 会检测初始化状态,在初始化期间直接返回,避免死锁。
|
||||
* @param defaultCategories 默认分类配置数组
|
||||
*/
|
||||
private async initializeDefaultCategories(): Promise<void> {
|
||||
async ensureDefaultCategories(
|
||||
defaultCategories: Array<{
|
||||
name: string;
|
||||
description?: string;
|
||||
color: string;
|
||||
}>
|
||||
): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
// ✅ 现在可以安全调用公共方法,ensureInitialized() 会智能跳过等待
|
||||
// ✅ 检查是否已初始化过默认分类
|
||||
const hasInitialized = await this.storageProvider.getItem('favorite_categories_initialized');
|
||||
if (hasInitialized === 'true') {
|
||||
return; // 已经初始化过,即使用户删光了也不再自动创建
|
||||
}
|
||||
|
||||
// ✅ 检查是否已有分类
|
||||
const existingCategories = await this.getCategories();
|
||||
|
||||
if (existingCategories.length === 0) {
|
||||
// ✅ 创建默认分类 - 可以安全调用 addCategory()
|
||||
const now = Date.now();
|
||||
const defaultCategories: FavoriteCategory[] = [
|
||||
{
|
||||
id: `cat_${now}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
name: '未分类',
|
||||
description: '默认分类',
|
||||
color: '#6B7280',
|
||||
sortOrder: 0,
|
||||
createdAt: now
|
||||
},
|
||||
{
|
||||
id: `cat_${now + 1}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
name: '系统提示词',
|
||||
description: '优化后的系统提示词',
|
||||
color: '#3B82F6',
|
||||
sortOrder: 1,
|
||||
createdAt: now + 1
|
||||
},
|
||||
{
|
||||
id: `cat_${now + 2}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
name: '用户提示词',
|
||||
description: '优化后的用户提示词',
|
||||
color: '#10B981',
|
||||
sortOrder: 2,
|
||||
createdAt: now + 2
|
||||
},
|
||||
{
|
||||
id: `cat_${now + 3}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
name: '创意写作',
|
||||
description: '创意写作相关的提示词',
|
||||
color: '#8B5CF6',
|
||||
sortOrder: 3,
|
||||
createdAt: now + 3
|
||||
},
|
||||
{
|
||||
id: `cat_${now + 4}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
name: '编程开发',
|
||||
description: '编程开发相关的提示词',
|
||||
color: '#F59E0B',
|
||||
sortOrder: 4,
|
||||
createdAt: now + 4
|
||||
},
|
||||
{
|
||||
id: `cat_${now + 5}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
name: '商业分析',
|
||||
description: '商业分析相关的提示词',
|
||||
color: '#EF4444',
|
||||
sortOrder: 5,
|
||||
createdAt: now + 5
|
||||
}
|
||||
];
|
||||
|
||||
// ✅ 批量添加默认分类 - 使用公共方法
|
||||
for (const category of defaultCategories) {
|
||||
// ✅ 首次使用,创建默认分类
|
||||
for (let i = 0; i < defaultCategories.length; i++) {
|
||||
const category = defaultCategories[i];
|
||||
await this.addCategory({
|
||||
name: category.name,
|
||||
description: category.description,
|
||||
color: category.color,
|
||||
sortOrder: category.sortOrder
|
||||
sortOrder: i
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 标记已初始化
|
||||
await this.storageProvider.setItem('favorite_categories_initialized', 'true');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('初始化默认分类失败:', error);
|
||||
console.warn('[FavoriteManager] 确保默认分类失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,24 +188,24 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
|
||||
// 验证输入
|
||||
if (!favorite.content?.trim()) {
|
||||
throw new FavoriteValidationError('提示词内容不能为空');
|
||||
throw new FavoriteValidationError('Prompt content cannot be empty');
|
||||
}
|
||||
|
||||
// 验证 functionMode 必填
|
||||
if (!favorite.functionMode) {
|
||||
throw new FavoriteValidationError('功能模式 (functionMode) 不能为空');
|
||||
throw new FavoriteValidationError('Function mode (functionMode) cannot be empty');
|
||||
}
|
||||
|
||||
// 验证功能模式分类的完整性
|
||||
if (favorite.functionMode === 'basic' || favorite.functionMode === 'context') {
|
||||
if (!favorite.optimizationMode) {
|
||||
throw new FavoriteValidationError(`${favorite.functionMode} 模式必须指定 optimizationMode`);
|
||||
throw new FavoriteValidationError(`${favorite.functionMode} mode must specify optimizationMode`);
|
||||
}
|
||||
}
|
||||
|
||||
if (favorite.functionMode === 'image') {
|
||||
if (!favorite.imageSubMode) {
|
||||
throw new FavoriteValidationError('image 模式必须指定 imageSubMode');
|
||||
throw new FavoriteValidationError('Image mode must specify imageSubMode');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,7 +247,7 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
throw error;
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new FavoriteStorageError(`添加收藏失败: ${errorMessage}`);
|
||||
throw new FavoriteStorageError(`Failed to add favorite: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +318,7 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
return favoritesList;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new FavoriteStorageError(`获取收藏列表失败: ${errorMessage}`);
|
||||
throw new FavoriteStorageError(`Failed to get favorites: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,7 +337,7 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
throw error;
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new FavoriteStorageError(`获取收藏详情失败: ${errorMessage}`);
|
||||
throw new FavoriteStorageError(`Failed to get favorite details: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,7 +367,7 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
throw error;
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new FavoriteStorageError(`更新收藏失败: ${errorMessage}`);
|
||||
throw new FavoriteStorageError(`Failed to update favorite: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,7 +391,7 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
throw error;
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new FavoriteStorageError(`删除收藏失败: ${errorMessage}`);
|
||||
throw new FavoriteStorageError(`Failed to delete favorite: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,7 +401,7 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
const favoritesList = favorites || [];
|
||||
const deletedCount = favoritesList.filter(f => ids.includes(f.id)).length;
|
||||
if (deletedCount === 0) {
|
||||
throw new FavoriteNotFoundError('未找到要删除的收藏项');
|
||||
throw new FavoriteNotFoundError('Favorite to delete not found');
|
||||
}
|
||||
|
||||
return favoritesList.filter(f => !ids.includes(f.id));
|
||||
@@ -437,7 +413,7 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
throw error;
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new FavoriteStorageError(`批量删除收藏失败: ${errorMessage}`);
|
||||
throw new FavoriteStorageError(`Failed to batch delete favorites: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,7 +434,7 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
return categories ? JSON.parse(categories) : [];
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new FavoriteStorageError(`获取分类列表失败: ${errorMessage}`);
|
||||
throw new FavoriteStorageError(`Failed to get categories: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -466,7 +442,7 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
await this.ensureInitialized();
|
||||
|
||||
if (!category.name?.trim()) {
|
||||
throw new FavoriteValidationError('分类名称不能为空');
|
||||
throw new FavoriteValidationError('Category name cannot be empty');
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
@@ -485,7 +461,7 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
// 检查是否已存在同名分类
|
||||
const existing = categoriesList.find(c => c.name === category.name);
|
||||
if (existing) {
|
||||
throw new FavoriteError(`分类已存在: ${category.name}`, 'CATEGORY_ALREADY_EXISTS');
|
||||
throw new FavoriteError(`Category already exists: ${category.name}`, 'CATEGORY_ALREADY_EXISTS');
|
||||
}
|
||||
return [...categoriesList, newCategory];
|
||||
});
|
||||
@@ -496,7 +472,7 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
throw error;
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new FavoriteStorageError(`添加分类失败: ${errorMessage}`);
|
||||
throw new FavoriteStorageError(`Failed to add category: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,18 +497,34 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
throw error;
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new FavoriteStorageError(`更新分类失败: ${errorMessage}`);
|
||||
throw new FavoriteStorageError(`Failed to update category: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteCategory(id: string): Promise<void> {
|
||||
/**
|
||||
* 删除分类
|
||||
* 会自动清空该分类下所有收藏的分类字段
|
||||
*
|
||||
* @param id 分类ID
|
||||
* @returns 受影响的收藏数量
|
||||
*/
|
||||
async deleteCategory(id: string): Promise<number> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
// 检查是否有收藏项使用此分类
|
||||
const favoritesInCategory = await this.getFavorites({ categoryId: id });
|
||||
if (favoritesInCategory.length > 0) {
|
||||
throw new FavoriteValidationError(`无法删除分类,还有 ${favoritesInCategory.length} 个收藏项使用此分类`);
|
||||
// ✅ 获取该分类下的所有收藏
|
||||
const allFavorites = await this.getFavorites();
|
||||
const favoritesInCategory = allFavorites.filter(f => f.category === id);
|
||||
|
||||
// ✅ 清空这些收藏的分类字段(不依赖"未分类"是否存在)
|
||||
for (const favorite of favoritesInCategory) {
|
||||
await this.updateFavorite(favorite.id, {
|
||||
...favorite,
|
||||
category: undefined // 清空分类
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 删除分类
|
||||
await this.storageProvider.updateData(this.STORAGE_KEYS.CATEGORIES, (categories: FavoriteCategory[] | null) => {
|
||||
const categoriesList = categories || [];
|
||||
const index = categoriesList.findIndex(c => c.id === id);
|
||||
@@ -542,12 +534,14 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
|
||||
return categoriesList.filter(c => c.id !== id);
|
||||
});
|
||||
|
||||
return favoritesInCategory.length;
|
||||
} catch (error) {
|
||||
if (error instanceof FavoriteError) {
|
||||
throw error;
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new FavoriteStorageError(`删除分类失败: ${errorMessage}`);
|
||||
throw new FavoriteStorageError(`Failed to delete category: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,7 +556,7 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
return await this.updateStats();
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new FavoriteStorageError(`获取统计信息失败: ${errorMessage}`);
|
||||
throw new FavoriteStorageError(`Failed to get statistics: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,6 +611,21 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取独立标签库中的所有标签名称
|
||||
* @private
|
||||
*/
|
||||
private async getAllIndependentTags(): Promise<string[]> {
|
||||
try {
|
||||
const storedTags = await this.storageProvider.getItem(this.STORAGE_KEYS.TAGS);
|
||||
const independentTags: FavoriteTag[] = storedTags ? JSON.parse(storedTags) : [];
|
||||
return independentTags.map(t => t.tag);
|
||||
} catch (error) {
|
||||
console.warn('获取独立标签失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async exportFavorites(ids?: string[]): Promise<string> {
|
||||
try {
|
||||
let favorites: FavoritePrompt[];
|
||||
@@ -628,44 +637,120 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
}
|
||||
|
||||
const categories = await this.getCategories();
|
||||
const tags = await this.getAllIndependentTags();
|
||||
|
||||
const exportData = {
|
||||
version: '1.0',
|
||||
exportDate: new Date().toISOString(),
|
||||
favorites,
|
||||
categories
|
||||
categories,
|
||||
tags // 导出独立标签库(包含所有标签:使用中的 + 预创建的)
|
||||
};
|
||||
|
||||
return JSON.stringify(exportData, null, 2);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new FavoriteStorageError(`导出收藏失败: ${errorMessage}`);
|
||||
throw new FavoriteImportExportError(
|
||||
`Failed to export favorites: ${errorMessage}`,
|
||||
error instanceof Error ? error : undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算标签使用统计
|
||||
* @private
|
||||
* @returns 包含标签名和使用次数的 Map
|
||||
*/
|
||||
private async computeTagCounts(): Promise<Map<string, number>> {
|
||||
// 1. 获取独立标签
|
||||
const storedTags = await this.storageProvider.getItem(this.STORAGE_KEYS.TAGS);
|
||||
const independentTags: FavoriteTag[] = storedTags ? JSON.parse(storedTags) : [];
|
||||
|
||||
// 2. 统计收藏项中使用的标签
|
||||
const favorites = await this.getFavorites();
|
||||
const tagCounts = new Map<string, number>();
|
||||
|
||||
favorites.forEach(favorite => {
|
||||
favorite.tags.forEach(tag => {
|
||||
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
|
||||
});
|
||||
});
|
||||
|
||||
// 3. 合并独立标签和使用中的标签
|
||||
// 独立标签如果未被使用,count 为 0
|
||||
independentTags.forEach(({ tag }) => {
|
||||
if (!tagCounts.has(tag)) {
|
||||
tagCounts.set(tag, 0);
|
||||
}
|
||||
});
|
||||
|
||||
return tagCounts;
|
||||
}
|
||||
|
||||
async getAllTags(): Promise<Array<{ tag: string; count: number }>> {
|
||||
try {
|
||||
const favorites = await this.getFavorites();
|
||||
const tagCounts = new Map<string, number>();
|
||||
|
||||
favorites.forEach(favorite => {
|
||||
favorite.tags.forEach(tag => {
|
||||
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
|
||||
});
|
||||
});
|
||||
const tagCounts = await this.computeTagCounts();
|
||||
|
||||
// 返回排序后的结果(使用次数降序,相同次数按标签名升序)
|
||||
return Array.from(tagCounts.entries())
|
||||
.map(([tag, count]) => ({ tag, count }))
|
||||
.sort((a, b) => b.count - a.count); // 按使用次数降序
|
||||
.sort((a, b) => {
|
||||
if (b.count !== a.count) {
|
||||
return b.count - a.count; // 按使用次数降序
|
||||
}
|
||||
return a.tag.localeCompare(b.tag); // 相同次数按标签名升序
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new FavoriteStorageError(`获取标签列表失败: ${errorMessage}`);
|
||||
throw new FavoriteStorageError(`Failed to get tags: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
async addTag(tag: string): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
const trimmedTag = tag.trim();
|
||||
if (!trimmedTag) {
|
||||
throw new FavoriteValidationError('Tag name cannot be empty');
|
||||
}
|
||||
|
||||
try {
|
||||
await this.storageProvider.updateData(this.STORAGE_KEYS.TAGS, (tags: FavoriteTag[] | null) => {
|
||||
const tagsList = tags || [];
|
||||
|
||||
// 检查是否已存在
|
||||
const existing = tagsList.find(t => t.tag === trimmedTag);
|
||||
if (existing) {
|
||||
throw new FavoriteTagAlreadyExistsError(trimmedTag);
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const newTag: FavoriteTag = {
|
||||
tag: trimmedTag,
|
||||
createdAt: now
|
||||
};
|
||||
|
||||
return [...tagsList, newTag];
|
||||
});
|
||||
|
||||
// 更新统计信息
|
||||
await this.updateStats();
|
||||
} catch (error) {
|
||||
if (error instanceof FavoriteError) {
|
||||
throw error;
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new FavoriteStorageError(
|
||||
`Failed to add tag: ${errorMessage}`,
|
||||
error instanceof Error ? error : undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async renameTag(oldTag: string, newTag: string): Promise<number> {
|
||||
if (!oldTag || !newTag) {
|
||||
throw new FavoriteValidationError('标签名不能为空');
|
||||
throw new FavoriteValidationError('Tag name cannot be empty');
|
||||
}
|
||||
|
||||
if (oldTag === newTag) {
|
||||
@@ -673,8 +758,21 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
}
|
||||
|
||||
let affectedCount = 0;
|
||||
let oldTagExistedInIndependentLib = false;
|
||||
|
||||
try {
|
||||
// 1. 更新独立标签库:删除旧标签,记录是否存在
|
||||
await this.storageProvider.updateData(this.STORAGE_KEYS.TAGS, (tags: FavoriteTag[] | null) => {
|
||||
const tagsList = tags || [];
|
||||
|
||||
// 检查旧标签是否存在
|
||||
oldTagExistedInIndependentLib = tagsList.some(t => t.tag === oldTag);
|
||||
|
||||
// 删除旧标签
|
||||
return tagsList.filter(t => t.tag !== oldTag);
|
||||
});
|
||||
|
||||
// 2. 更新收藏列表中的标签
|
||||
await this.storageProvider.updateData(this.STORAGE_KEYS.FAVORITES, (favorites: FavoritePrompt[] | null) => {
|
||||
const favoritesList = favorites || [];
|
||||
|
||||
@@ -695,26 +793,64 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
return favoritesList;
|
||||
});
|
||||
|
||||
// 3. 只有当旧标签存在于独立库或被收藏使用时,才添加新标签到独立库
|
||||
if (oldTagExistedInIndependentLib || affectedCount > 0) {
|
||||
await this.storageProvider.updateData(this.STORAGE_KEYS.TAGS, (tags: FavoriteTag[] | null) => {
|
||||
const tagsList = tags || [];
|
||||
|
||||
// 添加新标签(如果不存在)
|
||||
const hasNewTag = tagsList.some(t => t.tag === newTag);
|
||||
if (!hasNewTag) {
|
||||
tagsList.push({
|
||||
tag: newTag,
|
||||
createdAt: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
return tagsList;
|
||||
});
|
||||
}
|
||||
|
||||
await this.updateStats();
|
||||
return affectedCount;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new FavoriteStorageError(`重命名标签失败: ${errorMessage}`);
|
||||
throw new FavoriteStorageError(`Failed to rename tag: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
async mergeTags(sourceTags: string[], targetTag: string): Promise<number> {
|
||||
if (!sourceTags || sourceTags.length === 0) {
|
||||
throw new FavoriteValidationError('源标签列表不能为空');
|
||||
throw new FavoriteValidationError('Source tag list cannot be empty');
|
||||
}
|
||||
|
||||
if (!targetTag) {
|
||||
throw new FavoriteValidationError('目标标签不能为空');
|
||||
throw new FavoriteValidationError('Target tag cannot be empty');
|
||||
}
|
||||
|
||||
let affectedCount = 0;
|
||||
|
||||
try {
|
||||
// 1. 更新独立标签库:删除所有源标签,确保目标标签存在
|
||||
await this.storageProvider.updateData(this.STORAGE_KEYS.TAGS, (tags: FavoriteTag[] | null) => {
|
||||
const tagsList = tags || [];
|
||||
|
||||
// 删除所有源标签
|
||||
const filteredTags = tagsList.filter(t => !sourceTags.includes(t.tag));
|
||||
|
||||
// 确保目标标签存在
|
||||
const hasTargetTag = filteredTags.some(t => t.tag === targetTag);
|
||||
if (!hasTargetTag) {
|
||||
filteredTags.push({
|
||||
tag: targetTag,
|
||||
createdAt: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
return filteredTags;
|
||||
});
|
||||
|
||||
// 2. 更新收藏列表中的标签
|
||||
await this.storageProvider.updateData(this.STORAGE_KEYS.FAVORITES, (favorites: FavoritePrompt[] | null) => {
|
||||
const favoritesList = favorites || [];
|
||||
|
||||
@@ -747,18 +883,25 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
return affectedCount;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new FavoriteStorageError(`合并标签失败: ${errorMessage}`);
|
||||
throw new FavoriteStorageError(`Failed to merge tags: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteTag(tag: string): Promise<number> {
|
||||
if (!tag) {
|
||||
throw new FavoriteValidationError('标签名不能为空');
|
||||
throw new FavoriteValidationError('Tag name cannot be empty');
|
||||
}
|
||||
|
||||
let affectedCount = 0;
|
||||
|
||||
try {
|
||||
// 1. 从独立标签中删除
|
||||
await this.storageProvider.updateData(this.STORAGE_KEYS.TAGS, (tags: FavoriteTag[] | null) => {
|
||||
const tagsList = tags || [];
|
||||
return tagsList.filter(t => t.tag !== tag);
|
||||
});
|
||||
|
||||
// 2. 从所有收藏项中删除
|
||||
await this.storageProvider.updateData(this.STORAGE_KEYS.FAVORITES, (favorites: FavoritePrompt[] | null) => {
|
||||
const favoritesList = favorites || [];
|
||||
|
||||
@@ -778,13 +921,13 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
return affectedCount;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new FavoriteStorageError(`删除标签失败: ${errorMessage}`);
|
||||
throw new FavoriteStorageError(`Failed to delete tag: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
async reorderCategories(categoryIds: string[]): Promise<void> {
|
||||
if (!categoryIds || categoryIds.length === 0) {
|
||||
throw new FavoriteValidationError('分类ID列表不能为空');
|
||||
throw new FavoriteValidationError('Category ID list cannot be empty');
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -820,7 +963,7 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new FavoriteStorageError(`重新排序分类失败: ${errorMessage}`);
|
||||
throw new FavoriteStorageError(`Failed to reorder categories: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -830,7 +973,7 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
return favorites.length;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new FavoriteStorageError(`获取分类使用统计失败: ${errorMessage}`);
|
||||
throw new FavoriteStorageError(`Failed to get category usage: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -850,7 +993,43 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
const importData = JSON.parse(data);
|
||||
|
||||
if (!importData.favorites || !Array.isArray(importData.favorites)) {
|
||||
throw new FavoriteValidationError('导入数据格式无效');
|
||||
throw new FavoriteValidationError('Invalid import data format');
|
||||
}
|
||||
// 【新增】先导入分类(如果有)
|
||||
if (importData.categories && Array.isArray(importData.categories)) {
|
||||
for (const category of importData.categories) {
|
||||
try {
|
||||
// 检查分类是否已存在(根据ID或名称)
|
||||
const existingCategories = await this.getCategories();
|
||||
const exists = existingCategories.some(
|
||||
c => c.id === category.id || c.name === category.name
|
||||
);
|
||||
|
||||
if (!exists) {
|
||||
await this.addCategory({
|
||||
name: category.name,
|
||||
description: category.description,
|
||||
color: category.color,
|
||||
sortOrder: category.sortOrder
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// 分类导入失败,记录错误但继续
|
||||
console.warn('Failed to import category:', category.name, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 【新增】先导入独立标签(如果有)
|
||||
if (importData.tags && Array.isArray(importData.tags)) {
|
||||
for (const tag of importData.tags) {
|
||||
try {
|
||||
await this.addTag(tag);
|
||||
} catch (error) {
|
||||
// 标签已存在,跳过错误继续
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const existingFavorites = await this.getFavorites();
|
||||
@@ -860,7 +1039,7 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
try {
|
||||
// 验证必填字段
|
||||
if (!favorite.content?.trim()) {
|
||||
throw new FavoriteValidationError('导入数据中存在空内容的收藏项');
|
||||
throw new FavoriteValidationError('Import data contains favorite with empty content');
|
||||
}
|
||||
|
||||
// 构建功能模式数据,兼容旧数据
|
||||
@@ -872,7 +1051,7 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
const mapping = { functionMode, optimizationMode, imageSubMode };
|
||||
if (!TypeMapper.validateMapping(mapping)) {
|
||||
throw new FavoriteValidationError(
|
||||
`导入数据中存在无效的功能模式分类: functionMode=${functionMode}, optimizationMode=${optimizationMode}, imageSubMode=${imageSubMode}`
|
||||
`Invalid function mode in import data: functionMode=${functionMode}, optimizationMode=${optimizationMode}, imageSubMode=${imageSubMode}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -912,7 +1091,7 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
result.errors.push(`导入收藏失败: ${errorMessage}`);
|
||||
result.errors.push(`Failed to import favorite: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -920,7 +1099,11 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new FavoriteStorageError(`导入收藏失败: ${errorMessage}`);
|
||||
throw new FavoriteImportExportError(
|
||||
`Failed to import favorites: ${errorMessage}`,
|
||||
error instanceof Error ? error : undefined,
|
||||
result.errors.length > 0 ? result.errors : undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import type { TagStatistics } from './types';
|
||||
|
||||
/**
|
||||
* 标签类型转换工具
|
||||
* 统一处理不同格式之间的标签数据转换
|
||||
*/
|
||||
export class TagTypeConverter {
|
||||
/**
|
||||
* 将 API 返回的标签数据转换为 TagStatistics 格式
|
||||
* @param apiData API 返回的标签数据 { tag: string; count: number }[]
|
||||
* @returns TagStatistics[] 格式的标签统计数据
|
||||
*/
|
||||
static toTagStatistics(apiData: Array<{ tag: string; count: number }>): TagStatistics[] {
|
||||
return apiData.map(item => ({
|
||||
name: item.tag,
|
||||
count: item.count,
|
||||
lastUsed: undefined
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 TagStatistics 转换回 API 格式
|
||||
* @param stats TagStatistics[] 格式的标签统计数据
|
||||
* @returns API 格式的标签数据 { tag: string; count: number }[]
|
||||
*/
|
||||
static fromTagStatistics(stats: TagStatistics[]): Array<{ tag: string; count: number }> {
|
||||
return stats.map(item => ({
|
||||
tag: item.name,
|
||||
count: item.count
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将标签数据转换为自动完成选项格式
|
||||
* @param apiData API 返回的标签数据
|
||||
* @returns 自动完成选项格式 { label: string; value: string; count: number }[]
|
||||
*/
|
||||
static toAutoCompleteOptions(apiData: Array<{ tag: string; count: number }>): Array<{
|
||||
label: string;
|
||||
value: string;
|
||||
count: number;
|
||||
}> {
|
||||
return apiData.map(item => ({
|
||||
label: `${item.tag} (${item.count})`,
|
||||
value: item.tag,
|
||||
count: item.count
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将标签数据转换为简单的字符串数组
|
||||
* @param apiData API 返回的标签数据
|
||||
* @returns 标签名称数组
|
||||
*/
|
||||
static toStringArray(apiData: Array<{ tag: string; count: number }>): string[] {
|
||||
return apiData.map(item => item.tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按使用次数降序排序标签
|
||||
* @param tags 标签数据
|
||||
* @returns 排序后的标签数据
|
||||
*/
|
||||
static sortByCount<T extends { count: number }>(tags: T[]): T[] {
|
||||
return [...tags].sort((a, b) => b.count - a.count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按标签名称升序排序标签
|
||||
* @param tags 标签数据
|
||||
* @returns 排序后的标签数据
|
||||
*/
|
||||
static sortByName(tags: TagStatistics[]): TagStatistics[] {
|
||||
return [...tags].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
/**
|
||||
* 混合排序:先按使用次数降序,相同次数按名称升序
|
||||
* @param tags 标签数据
|
||||
* @returns 排序后的标签数据
|
||||
*/
|
||||
static sortByCountThenName(tags: TagStatistics[]): TagStatistics[] {
|
||||
return [...tags].sort((a, b) => {
|
||||
if (b.count !== a.count) {
|
||||
return b.count - a.count;
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -84,6 +84,29 @@ export interface FavoriteStats {
|
||||
lastUsedAt?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 独立标签接口
|
||||
*/
|
||||
export interface FavoriteTag {
|
||||
/** 标签名称 */
|
||||
tag: string;
|
||||
/** 创建时间 */
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签统计信息接口
|
||||
* 用于标签管理器展示标签使用情况
|
||||
*/
|
||||
export interface TagStatistics {
|
||||
/** 标签名称 */
|
||||
name: string;
|
||||
/** 使用次数 */
|
||||
count: number;
|
||||
/** 最后使用时间(可选,暂未实现) */
|
||||
lastUsed?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收藏管理器接口
|
||||
*/
|
||||
@@ -127,7 +150,7 @@ export interface IFavoriteManager {
|
||||
updateCategory(id: string, updates: Partial<FavoriteCategory>): Promise<void>;
|
||||
|
||||
/** 删除分类 */
|
||||
deleteCategory(id: string): Promise<void>;
|
||||
deleteCategory(id: string): Promise<number>;
|
||||
|
||||
/** 获取统计信息 */
|
||||
getStats(): Promise<FavoriteStats>;
|
||||
@@ -151,16 +174,19 @@ export interface IFavoriteManager {
|
||||
errors: string[];
|
||||
}>;
|
||||
|
||||
/** 获取所有标签及其使用统计 */
|
||||
/** 获取所有标签及其使用统计(包含独立标签和使用中的标签) */
|
||||
getAllTags(): Promise<Array<{ tag: string; count: number }>>;
|
||||
|
||||
/** 添加独立标签 */
|
||||
addTag(tag: string): Promise<void>;
|
||||
|
||||
/** 重命名标签 */
|
||||
renameTag(oldTag: string, newTag: string): Promise<number>;
|
||||
|
||||
/** 合并多个标签为一个 */
|
||||
mergeTags(sourceTags: string[], targetTag: string): Promise<number>;
|
||||
|
||||
/** 删除标签 */
|
||||
/** 删除标签(同时从独立标签和所有收藏项中删除) */
|
||||
deleteTag(tag: string): Promise<number>;
|
||||
|
||||
/** 对分类进行重新排序 */
|
||||
|
||||
@@ -0,0 +1,741 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { FavoriteManager } from '../../../src/services/favorite/manager';
|
||||
import type { IStorageProvider } from '../../../src/services/storage/types';
|
||||
import { TypeMapper } from '../../../src/services/favorite/type-mapper';
|
||||
import type { PromptRecordType } from '../../../src/services/history/types';
|
||||
|
||||
/**
|
||||
* FavoriteManager 集成测试
|
||||
* 测试完整的业务流程和跨功能交互
|
||||
*/
|
||||
describe('FavoriteManager - 集成测试', () => {
|
||||
let manager: FavoriteManager;
|
||||
let mockStorage: Map<string, string>;
|
||||
let storageProvider: IStorageProvider;
|
||||
|
||||
beforeEach(() => {
|
||||
// 创建模拟存储
|
||||
mockStorage = new Map<string, string>();
|
||||
|
||||
storageProvider = {
|
||||
getItem: vi.fn(async (key: string) => mockStorage.get(key) || null),
|
||||
setItem: vi.fn(async (key: string, value: string) => {
|
||||
mockStorage.set(key, value);
|
||||
}),
|
||||
removeItem: vi.fn(async (key: string) => {
|
||||
mockStorage.delete(key);
|
||||
}),
|
||||
clearAll: vi.fn(async () => {
|
||||
mockStorage.clear();
|
||||
}),
|
||||
batchUpdate: vi.fn(async (operations: Array<{ key: string; operation: 'set' | 'remove'; value?: string }>) => {
|
||||
operations.forEach(({ key, operation, value }) => {
|
||||
if (operation === 'set' && value) {
|
||||
mockStorage.set(key, value);
|
||||
} else if (operation === 'remove') {
|
||||
mockStorage.delete(key);
|
||||
}
|
||||
});
|
||||
}),
|
||||
updateData: vi.fn(async (key: string, updater: (data: any) => any) => {
|
||||
const currentData = mockStorage.get(key);
|
||||
const parsedData = currentData ? JSON.parse(currentData) : null;
|
||||
const updatedData = updater(parsedData);
|
||||
mockStorage.set(key, JSON.stringify(updatedData));
|
||||
})
|
||||
};
|
||||
|
||||
manager = new FavoriteManager(storageProvider);
|
||||
});
|
||||
|
||||
describe('收藏 CRUD 完整流程', () => {
|
||||
it('应该完成完整的增删改查流程', async () => {
|
||||
// 1. 创建分类
|
||||
const categoryId = await manager.addCategory({
|
||||
name: '测试分类',
|
||||
description: '用于集成测试',
|
||||
color: '#FF5722'
|
||||
});
|
||||
|
||||
// 2. 添加收藏
|
||||
const favoriteId = await manager.addFavorite({
|
||||
title: '测试收藏',
|
||||
content: '测试内容',
|
||||
tags: ['测试', '集成'],
|
||||
category: categoryId,
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system',
|
||||
metadata: {
|
||||
originalContent: '原始内容',
|
||||
sourceHistoryId: 'history-001'
|
||||
}
|
||||
});
|
||||
|
||||
expect(favoriteId).toBeTruthy();
|
||||
|
||||
// 3. 查询单个收藏
|
||||
const favorite = await manager.getFavorite(favoriteId);
|
||||
expect(favorite.id).toBe(favoriteId);
|
||||
expect(favorite.title).toBe('测试收藏');
|
||||
expect(favorite.tags).toEqual(['测试', '集成']);
|
||||
expect(favorite.category).toBe(categoryId);
|
||||
expect(favorite.functionMode).toBe('basic');
|
||||
expect(favorite.optimizationMode).toBe('system');
|
||||
expect(favorite.metadata?.originalContent).toBe('原始内容');
|
||||
expect(favorite.metadata?.sourceHistoryId).toBe('history-001');
|
||||
|
||||
// 4. 查询列表
|
||||
const favorites = await manager.getFavorites();
|
||||
expect(favorites.length).toBe(1);
|
||||
expect(favorites[0].id).toBe(favoriteId);
|
||||
|
||||
// 5. 更新收藏
|
||||
await manager.updateFavorite(favoriteId, {
|
||||
title: '更新后的标题',
|
||||
tags: ['测试', '集成', '更新'],
|
||||
functionMode: 'context',
|
||||
optimizationMode: 'user'
|
||||
});
|
||||
|
||||
const updated = await manager.getFavorite(favoriteId);
|
||||
expect(updated.title).toBe('更新后的标题');
|
||||
expect(updated.tags).toEqual(['测试', '集成', '更新']);
|
||||
expect(updated.functionMode).toBe('context');
|
||||
expect(updated.optimizationMode).toBe('user');
|
||||
expect(updated.updatedAt).not.toBe(favorite.updatedAt);
|
||||
|
||||
// 6. 删除收藏
|
||||
await manager.deleteFavorite(favoriteId);
|
||||
const allFavorites = await manager.getFavorites();
|
||||
expect(allFavorites.length).toBe(0);
|
||||
|
||||
// 7. 验证分类仍然存在
|
||||
const categories = await manager.getCategories();
|
||||
expect(categories.find(c => c.id === categoryId)).toBeDefined();
|
||||
});
|
||||
|
||||
it('应该正确处理多个收藏的关联关系', async () => {
|
||||
// 创建2个分类
|
||||
const cat1Id = await manager.addCategory({ name: '分类1', color: '#FF0000' });
|
||||
const cat2Id = await manager.addCategory({ name: '分类2', color: '#00FF00' });
|
||||
|
||||
// 创建3个收藏
|
||||
const fav1Id = await manager.addFavorite({
|
||||
title: '收藏1',
|
||||
content: '内容1',
|
||||
tags: ['共享标签', '标签1'],
|
||||
category: cat1Id,
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
const fav2Id = await manager.addFavorite({
|
||||
title: '收藏2',
|
||||
content: '内容2',
|
||||
tags: ['共享标签', '标签2'],
|
||||
category: cat1Id,
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'user'
|
||||
});
|
||||
|
||||
const fav3Id = await manager.addFavorite({
|
||||
title: '收藏3',
|
||||
content: '内容3',
|
||||
tags: ['标签3'],
|
||||
category: cat2Id,
|
||||
functionMode: 'image',
|
||||
imageSubMode: 'text2image'
|
||||
});
|
||||
|
||||
// 验证标签统计
|
||||
const tags = await manager.getAllTags();
|
||||
const sharedTag = tags.find(t => t.tag === '共享标签');
|
||||
expect(sharedTag?.count).toBe(2);
|
||||
|
||||
// 验证分类使用统计
|
||||
const cat1Usage = await manager.getCategoryUsage(cat1Id);
|
||||
const cat2Usage = await manager.getCategoryUsage(cat2Id);
|
||||
expect(cat1Usage).toBe(2);
|
||||
expect(cat2Usage).toBe(1);
|
||||
|
||||
// 按分类查询
|
||||
const cat1Favorites = await manager.getFavorites({ categoryId: cat1Id });
|
||||
expect(cat1Favorites.length).toBe(2);
|
||||
|
||||
// 按标签查询
|
||||
const sharedTagFavorites = await manager.getFavorites({ tags: ['共享标签'] });
|
||||
expect(sharedTagFavorites.length).toBe(2);
|
||||
});
|
||||
|
||||
it('应该支持功能模式验证的完整流程', async () => {
|
||||
// 添加各种模式的收藏
|
||||
const basicId = await manager.addFavorite({
|
||||
title: 'Basic Mode',
|
||||
content: 'Content',
|
||||
tags: [],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
const contextId = await manager.addFavorite({
|
||||
title: 'Context Mode',
|
||||
content: 'Content',
|
||||
tags: [],
|
||||
functionMode: 'context',
|
||||
optimizationMode: 'user'
|
||||
});
|
||||
|
||||
const imageId = await manager.addFavorite({
|
||||
title: 'Image Mode',
|
||||
content: 'Content',
|
||||
tags: [],
|
||||
functionMode: 'image',
|
||||
imageSubMode: 'text2image'
|
||||
});
|
||||
|
||||
// 验证每个收藏的模式
|
||||
const basic = await manager.getFavorite(basicId);
|
||||
expect(basic.functionMode).toBe('basic');
|
||||
expect(basic.optimizationMode).toBe('system');
|
||||
expect(basic.imageSubMode).toBeUndefined();
|
||||
|
||||
const context = await manager.getFavorite(contextId);
|
||||
expect(context.functionMode).toBe('context');
|
||||
expect(context.optimizationMode).toBe('user');
|
||||
expect(context.imageSubMode).toBeUndefined();
|
||||
|
||||
const image = await manager.getFavorite(imageId);
|
||||
expect(image.functionMode).toBe('image');
|
||||
expect(image.imageSubMode).toBe('text2image');
|
||||
expect(image.optimizationMode).toBeUndefined();
|
||||
|
||||
// 更新模式应该生效
|
||||
await manager.updateFavorite(basicId, {
|
||||
functionMode: 'context',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
const updated = await manager.getFavorite(basicId);
|
||||
expect(updated.functionMode).toBe('context');
|
||||
expect(updated.optimizationMode).toBe('system');
|
||||
});
|
||||
});
|
||||
|
||||
describe('从优化历史保存收藏集成测试', () => {
|
||||
it('应该从 optimize 类型正确创建收藏', async () => {
|
||||
// 模拟历史记录
|
||||
const recordType: PromptRecordType = 'optimize';
|
||||
const mapping = TypeMapper.mapFromRecordType(recordType);
|
||||
|
||||
// 创建收藏
|
||||
const favoriteId = await manager.addFavorite({
|
||||
title: '优化后的提示词',
|
||||
content: '这是优化后的内容',
|
||||
tags: ['AI', '优化'],
|
||||
...mapping,
|
||||
metadata: {
|
||||
originalContent: '这是原始内容',
|
||||
sourceHistoryId: 'hist-123'
|
||||
}
|
||||
});
|
||||
|
||||
// 验证
|
||||
const favorite = await manager.getFavorite(favoriteId);
|
||||
expect(favorite.functionMode).toBe('basic');
|
||||
expect(favorite.optimizationMode).toBe('system');
|
||||
expect(favorite.metadata?.originalContent).toBe('这是原始内容');
|
||||
expect(favorite.metadata?.sourceHistoryId).toBe('hist-123');
|
||||
});
|
||||
|
||||
it('应该从 contextUserOptimize 类型正确创建收藏', async () => {
|
||||
const recordType: PromptRecordType = 'contextUserOptimize';
|
||||
const mapping = TypeMapper.mapFromRecordType(recordType);
|
||||
|
||||
const favoriteId = await manager.addFavorite({
|
||||
title: '用户上下文优化',
|
||||
content: '优化内容',
|
||||
tags: [],
|
||||
...mapping,
|
||||
metadata: {
|
||||
originalContent: '原始内容',
|
||||
sourceHistoryId: 'hist-456'
|
||||
}
|
||||
});
|
||||
|
||||
const favorite = await manager.getFavorite(favoriteId);
|
||||
expect(favorite.functionMode).toBe('context');
|
||||
expect(favorite.optimizationMode).toBe('user');
|
||||
});
|
||||
|
||||
it('应该从 imageOptimize 类型正确创建收藏', async () => {
|
||||
const recordType: PromptRecordType = 'imageOptimize';
|
||||
const mapping = TypeMapper.mapFromRecordType(recordType);
|
||||
|
||||
const favoriteId = await manager.addFavorite({
|
||||
title: '图像提示词优化',
|
||||
content: '优化后的图像提示词',
|
||||
tags: ['图像生成'],
|
||||
...mapping
|
||||
});
|
||||
|
||||
const favorite = await manager.getFavorite(favoriteId);
|
||||
expect(favorite.functionMode).toBe('image');
|
||||
expect(favorite.imageSubMode).toBe('text2image');
|
||||
});
|
||||
|
||||
it('应该从 image2imageOptimize 类型正确创建收藏', async () => {
|
||||
const recordType: PromptRecordType = 'image2imageOptimize';
|
||||
const mapping = TypeMapper.mapFromRecordType(recordType);
|
||||
|
||||
const favoriteId = await manager.addFavorite({
|
||||
title: '图生图提示词',
|
||||
content: '图生图优化内容',
|
||||
tags: [],
|
||||
...mapping
|
||||
});
|
||||
|
||||
const favorite = await manager.getFavorite(favoriteId);
|
||||
expect(favorite.functionMode).toBe('image');
|
||||
expect(favorite.imageSubMode).toBe('image2image');
|
||||
});
|
||||
|
||||
it('应该处理所有历史记录类型', async () => {
|
||||
const allTypes: PromptRecordType[] = [
|
||||
'optimize',
|
||||
'userOptimize',
|
||||
'iterate',
|
||||
'test',
|
||||
'contextSystemOptimize',
|
||||
'contextUserOptimize',
|
||||
'contextIterate',
|
||||
'imageOptimize',
|
||||
'contextImageOptimize',
|
||||
'imageIterate',
|
||||
'text2imageOptimize',
|
||||
'image2imageOptimize'
|
||||
];
|
||||
|
||||
const ids: string[] = [];
|
||||
|
||||
for (const type of allTypes) {
|
||||
const mapping = TypeMapper.mapFromRecordType(type);
|
||||
const id = await manager.addFavorite({
|
||||
title: `收藏-${type}`,
|
||||
content: `内容-${type}`,
|
||||
tags: [type],
|
||||
...mapping
|
||||
});
|
||||
ids.push(id);
|
||||
}
|
||||
|
||||
// 验证所有收藏都创建成功
|
||||
expect(ids.length).toBe(allTypes.length);
|
||||
|
||||
const favorites = await manager.getFavorites();
|
||||
expect(favorites.length).toBe(allTypes.length);
|
||||
|
||||
// 验证每个收藏的功能模式都有效
|
||||
for (const favorite of favorites) {
|
||||
const mapping = {
|
||||
functionMode: favorite.functionMode,
|
||||
optimizationMode: favorite.optimizationMode,
|
||||
imageSubMode: favorite.imageSubMode
|
||||
};
|
||||
expect(TypeMapper.validateMapping(mapping)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('标签和分类管理集成测试', () => {
|
||||
it('标签重命名应该更新所有关联收藏', async () => {
|
||||
// 创建多个使用相同标签的收藏
|
||||
await manager.addFavorite({
|
||||
title: '收藏1',
|
||||
content: '内容1',
|
||||
tags: ['旧标签', '其他标签'],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
await manager.addFavorite({
|
||||
title: '收藏2',
|
||||
content: '内容2',
|
||||
tags: ['旧标签'],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'user'
|
||||
});
|
||||
|
||||
await manager.addFavorite({
|
||||
title: '收藏3',
|
||||
content: '内容3',
|
||||
tags: ['不相关标签'],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
// 重命名标签
|
||||
await manager.renameTag('旧标签', '新标签');
|
||||
|
||||
// 验证所有收藏更新
|
||||
const favorites = await manager.getFavorites();
|
||||
const fav1 = favorites.find(f => f.title === '收藏1');
|
||||
const fav2 = favorites.find(f => f.title === '收藏2');
|
||||
const fav3 = favorites.find(f => f.title === '收藏3');
|
||||
|
||||
expect(fav1?.tags).toContain('新标签');
|
||||
expect(fav1?.tags).not.toContain('旧标签');
|
||||
expect(fav1?.tags).toContain('其他标签');
|
||||
|
||||
expect(fav2?.tags).toContain('新标签');
|
||||
expect(fav2?.tags).not.toContain('旧标签');
|
||||
|
||||
expect(fav3?.tags).toContain('不相关标签');
|
||||
expect(fav3?.tags).not.toContain('新标签');
|
||||
|
||||
// 验证标签统计
|
||||
const tags = await manager.getAllTags();
|
||||
const newTag = tags.find(t => t.tag === '新标签');
|
||||
const oldTag = tags.find(t => t.tag === '旧标签');
|
||||
|
||||
expect(newTag?.count).toBe(2);
|
||||
expect(oldTag).toBeUndefined();
|
||||
});
|
||||
|
||||
it('标签合并应该正确去重', async () => {
|
||||
// 创建测试数据
|
||||
await manager.addFavorite({
|
||||
title: '收藏1',
|
||||
content: '内容',
|
||||
tags: ['标签A', '标签B'],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
await manager.addFavorite({
|
||||
title: '收藏2',
|
||||
content: '内容',
|
||||
tags: ['标签A', '标签C'],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
await manager.addFavorite({
|
||||
title: '收藏3',
|
||||
content: '内容',
|
||||
tags: ['标签B', '标签C'],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
// 合并标签A和标签B -> 标签B
|
||||
await manager.mergeTags(['标签A'], '标签B');
|
||||
|
||||
const favorites = await manager.getFavorites();
|
||||
const fav1 = favorites.find(f => f.title === '收藏1');
|
||||
const fav2 = favorites.find(f => f.title === '收藏2');
|
||||
const fav3 = favorites.find(f => f.title === '收藏3');
|
||||
|
||||
// 收藏1: 原本 [A, B] -> [B] (去重)
|
||||
expect(fav1?.tags).toEqual(['标签B']);
|
||||
|
||||
// 收藏2: 原本 [A, C] -> [B, C]
|
||||
expect(fav2?.tags).toContain('标签B');
|
||||
expect(fav2?.tags).toContain('标签C');
|
||||
expect(fav2?.tags).not.toContain('标签A');
|
||||
|
||||
// 收藏3: 原本 [B, C] -> [B, C] (不变)
|
||||
expect(fav3?.tags).toContain('标签B');
|
||||
expect(fav3?.tags).toContain('标签C');
|
||||
|
||||
// 验证标签统计
|
||||
const tags = await manager.getAllTags();
|
||||
const tagA = tags.find(t => t.tag === '标签A');
|
||||
const tagB = tags.find(t => t.tag === '标签B');
|
||||
|
||||
expect(tagA).toBeUndefined();
|
||||
expect(tagB?.count).toBe(3); // 所有3个收藏都有标签B
|
||||
});
|
||||
|
||||
it('标签删除应该从所有收藏中移除', async () => {
|
||||
await manager.addFavorite({
|
||||
title: '收藏1',
|
||||
content: '内容',
|
||||
tags: ['要删除', '保留'],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
await manager.addFavorite({
|
||||
title: '收藏2',
|
||||
content: '内容',
|
||||
tags: ['要删除'],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
await manager.deleteTag('要删除');
|
||||
|
||||
const favorites = await manager.getFavorites();
|
||||
favorites.forEach(fav => {
|
||||
expect(fav.tags).not.toContain('要删除');
|
||||
});
|
||||
|
||||
const tags = await manager.getAllTags();
|
||||
expect(tags.find(t => t.tag === '要删除')).toBeUndefined();
|
||||
expect(tags.find(t => t.tag === '保留')).toBeDefined();
|
||||
});
|
||||
|
||||
it('分类排序应该更新所有分类的 sortOrder', async () => {
|
||||
const cat1 = await manager.addCategory({ name: '分类1', color: '#FF0000' });
|
||||
const cat2 = await manager.addCategory({ name: '分类2', color: '#00FF00' });
|
||||
const cat3 = await manager.addCategory({ name: '分类3', color: '#0000FF' });
|
||||
|
||||
// 新顺序: 3, 1, 2
|
||||
await manager.reorderCategories([cat3, cat1, cat2]);
|
||||
|
||||
const categories = await manager.getCategories();
|
||||
const sorted = categories.sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
|
||||
expect(sorted[0].id).toBe(cat3);
|
||||
expect(sorted[0].sortOrder).toBe(0);
|
||||
expect(sorted[1].id).toBe(cat1);
|
||||
expect(sorted[1].sortOrder).toBe(1);
|
||||
expect(sorted[2].id).toBe(cat2);
|
||||
expect(sorted[2].sortOrder).toBe(2);
|
||||
});
|
||||
|
||||
it('分类使用统计应该实时更新', async () => {
|
||||
const catId = await manager.addCategory({ name: '测试分类', color: '#FF0000' });
|
||||
|
||||
// 初始使用为0
|
||||
expect(await manager.getCategoryUsage(catId)).toBe(0);
|
||||
|
||||
// 添加收藏
|
||||
const fav1 = await manager.addFavorite({
|
||||
title: '收藏1',
|
||||
content: '内容',
|
||||
tags: [],
|
||||
category: catId,
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
expect(await manager.getCategoryUsage(catId)).toBe(1);
|
||||
|
||||
const fav2 = await manager.addFavorite({
|
||||
title: '收藏2',
|
||||
content: '内容',
|
||||
tags: [],
|
||||
category: catId,
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
expect(await manager.getCategoryUsage(catId)).toBe(2);
|
||||
|
||||
// 删除一个收藏
|
||||
await manager.deleteFavorite(fav1);
|
||||
expect(await manager.getCategoryUsage(catId)).toBe(1);
|
||||
|
||||
// 更新收藏移除分类
|
||||
await manager.updateFavorite(fav2, { category: undefined });
|
||||
expect(await manager.getCategoryUsage(catId)).toBe(0);
|
||||
});
|
||||
|
||||
it('删除分类应该清空关联收藏的分类字段', async () => {
|
||||
const catId = await manager.addCategory({ name: '要删除的分类', color: '#FF0000' });
|
||||
|
||||
const fav1 = await manager.addFavorite({
|
||||
title: '收藏1',
|
||||
content: '内容',
|
||||
tags: [],
|
||||
category: catId,
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
const fav2 = await manager.addFavorite({
|
||||
title: '收藏2',
|
||||
content: '内容',
|
||||
tags: [],
|
||||
category: catId,
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
const affectedCount = await manager.deleteCategory(catId);
|
||||
expect(affectedCount).toBe(2);
|
||||
|
||||
const updated1 = await manager.getFavorite(fav1);
|
||||
const updated2 = await manager.getFavorite(fav2);
|
||||
|
||||
expect(updated1.category).toBeUndefined();
|
||||
expect(updated2.category).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('导入导出集成测试', () => {
|
||||
it('应该正确导出和导入包含所有关联数据', async () => {
|
||||
// 创建完整的测试数据
|
||||
const catId = await manager.addCategory({
|
||||
name: '测试分类',
|
||||
description: '描述',
|
||||
color: '#FF5722'
|
||||
});
|
||||
|
||||
await manager.addFavorite({
|
||||
title: '完整收藏',
|
||||
content: '内容',
|
||||
tags: ['标签1', '标签2'],
|
||||
category: catId,
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system',
|
||||
metadata: {
|
||||
originalContent: '原始内容',
|
||||
sourceHistoryId: 'hist-001',
|
||||
customField: '自定义值'
|
||||
}
|
||||
});
|
||||
|
||||
// 导出
|
||||
const exportData = await manager.exportFavorites();
|
||||
|
||||
// 清空数据
|
||||
await storageProvider.clearAll();
|
||||
|
||||
// 导入
|
||||
await manager.importFavorites(exportData);
|
||||
|
||||
// 验证
|
||||
const favorites = await manager.getFavorites();
|
||||
expect(favorites.length).toBe(1);
|
||||
|
||||
const favorite = favorites[0];
|
||||
expect(favorite.title).toBe('完整收藏');
|
||||
expect(favorite.tags).toEqual(['标签1', '标签2']);
|
||||
expect(favorite.category).toBe(catId);
|
||||
expect(favorite.functionMode).toBe('basic');
|
||||
expect(favorite.optimizationMode).toBe('system');
|
||||
expect(favorite.metadata?.originalContent).toBe('原始内容');
|
||||
expect(favorite.metadata?.sourceHistoryId).toBe('hist-001');
|
||||
expect(favorite.metadata?.customField).toBe('自定义值');
|
||||
|
||||
const categories = await manager.getCategories();
|
||||
expect(categories.length).toBe(1);
|
||||
expect(categories[0].name).toBe('测试分类');
|
||||
});
|
||||
|
||||
it('应该正确处理导入时的分类和标签关联', async () => {
|
||||
const cat1 = await manager.addCategory({ name: '分类1', color: '#FF0000' });
|
||||
const cat2 = await manager.addCategory({ name: '分类2', color: '#00FF00' });
|
||||
|
||||
await manager.addFavorite({
|
||||
title: '收藏1',
|
||||
content: '内容1',
|
||||
tags: ['共享', 'A'],
|
||||
category: cat1,
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
await manager.addFavorite({
|
||||
title: '收藏2',
|
||||
content: '内容2',
|
||||
tags: ['共享', 'B'],
|
||||
category: cat2,
|
||||
functionMode: 'context',
|
||||
optimizationMode: 'user'
|
||||
});
|
||||
|
||||
const exportData = await manager.exportFavorites();
|
||||
await storageProvider.clearAll();
|
||||
await manager.importFavorites(exportData);
|
||||
|
||||
// 验证分类
|
||||
const categories = await manager.getCategories();
|
||||
expect(categories.length).toBe(2);
|
||||
|
||||
// 验证标签
|
||||
const tags = await manager.getAllTags();
|
||||
const sharedTag = tags.find(t => t.tag === '共享');
|
||||
expect(sharedTag?.count).toBe(2);
|
||||
|
||||
// 验证收藏
|
||||
const favorites = await manager.getFavorites();
|
||||
expect(favorites.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('搜索和过滤集成测试', () => {
|
||||
beforeEach(async () => {
|
||||
// 创建测试数据集
|
||||
const cat1 = await manager.addCategory({ name: 'AI工具', color: '#FF0000' });
|
||||
const cat2 = await manager.addCategory({ name: '写作助手', color: '#00FF00' });
|
||||
|
||||
await manager.addFavorite({
|
||||
title: 'ChatGPT提示词',
|
||||
content: '帮助我写一个关于AI的文章',
|
||||
tags: ['AI', 'ChatGPT'],
|
||||
category: cat1,
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
await manager.addFavorite({
|
||||
title: '创意写作助手',
|
||||
content: '帮我生成创意故事大纲',
|
||||
tags: ['写作', '创意'],
|
||||
category: cat2,
|
||||
functionMode: 'context',
|
||||
optimizationMode: 'user'
|
||||
});
|
||||
|
||||
await manager.addFavorite({
|
||||
title: 'AI绘图提示词',
|
||||
content: 'a beautiful sunset over mountains',
|
||||
tags: ['AI', '绘图'],
|
||||
category: cat1,
|
||||
functionMode: 'image',
|
||||
imageSubMode: 'text2image'
|
||||
});
|
||||
});
|
||||
|
||||
it('应该能按关键词搜索', async () => {
|
||||
const results = await manager.searchFavorites('AI');
|
||||
expect(results.length).toBe(2);
|
||||
expect(results.every(f => f.title.includes('AI') || f.content.includes('AI'))).toBe(true);
|
||||
});
|
||||
|
||||
it('应该能按分类过滤', async () => {
|
||||
const categories = await manager.getCategories();
|
||||
const aiCategory = categories.find(c => c.name === 'AI工具');
|
||||
|
||||
const results = await manager.getFavorites({ categoryId: aiCategory!.id });
|
||||
expect(results.length).toBe(2);
|
||||
expect(results.every(f => f.category === aiCategory!.id)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该能按标签过滤', async () => {
|
||||
const results = await manager.getFavorites({ tags: ['AI'] });
|
||||
expect(results.length).toBe(2);
|
||||
expect(results.every(f => f.tags.includes('AI'))).toBe(true);
|
||||
});
|
||||
|
||||
it('应该支持组合过滤', async () => {
|
||||
const categories = await manager.getCategories();
|
||||
const aiCategory = categories.find(c => c.name === 'AI工具');
|
||||
|
||||
const results = await manager.searchFavorites('提示词', {
|
||||
tags: ['AI'],
|
||||
categoryId: aiCategory!.id
|
||||
});
|
||||
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
results.forEach(f => {
|
||||
expect(f.title.includes('提示词') || f.content.includes('提示词')).toBe(true);
|
||||
expect(f.tags.includes('AI')).toBe(true);
|
||||
expect(f.category).toBe(aiCategory!.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,515 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { FavoriteManager } from '../../../src/services/favorite/manager';
|
||||
import type { IStorageProvider } from '../../../src/services/storage/types';
|
||||
import { FavoriteValidationError } from '../../../src/services/favorite/errors';
|
||||
import { TypeMapper } from '../../../src/services/favorite/type-mapper';
|
||||
|
||||
/**
|
||||
* FavoriteManager 扩展功能单元测试
|
||||
* 测试 functionMode 验证、分类管理等功能
|
||||
*/
|
||||
describe('FavoriteManager - 扩展功能', () => {
|
||||
let manager: FavoriteManager;
|
||||
let mockStorage: Map<string, string>;
|
||||
let storageProvider: IStorageProvider;
|
||||
|
||||
beforeEach(() => {
|
||||
// 创建模拟存储
|
||||
mockStorage = new Map<string, string>();
|
||||
|
||||
storageProvider = {
|
||||
getItem: vi.fn(async (key: string) => mockStorage.get(key) || null),
|
||||
setItem: vi.fn(async (key: string, value: string) => {
|
||||
mockStorage.set(key, value);
|
||||
}),
|
||||
removeItem: vi.fn(async (key: string) => {
|
||||
mockStorage.delete(key);
|
||||
}),
|
||||
clearAll: vi.fn(async () => {
|
||||
mockStorage.clear();
|
||||
}),
|
||||
batchUpdate: vi.fn(async (operations: Array<{ key: string; operation: 'set' | 'remove'; value?: string }>) => {
|
||||
operations.forEach(({ key, operation, value }) => {
|
||||
if (operation === 'set' && value) {
|
||||
mockStorage.set(key, value);
|
||||
} else if (operation === 'remove') {
|
||||
mockStorage.delete(key);
|
||||
}
|
||||
});
|
||||
}),
|
||||
updateData: vi.fn(async (key: string, updater: (data: any) => any) => {
|
||||
const currentData = mockStorage.get(key);
|
||||
const parsedData = currentData ? JSON.parse(currentData) : null;
|
||||
const updatedData = updater(parsedData);
|
||||
mockStorage.set(key, JSON.stringify(updatedData));
|
||||
})
|
||||
};
|
||||
|
||||
manager = new FavoriteManager(storageProvider);
|
||||
});
|
||||
|
||||
describe('addFavorite - functionMode 验证', () => {
|
||||
it('应该拒绝缺少 functionMode 的收藏', async () => {
|
||||
await expect(
|
||||
manager.addFavorite({
|
||||
title: '测试',
|
||||
content: '内容',
|
||||
tags: [],
|
||||
// @ts-expect-error 故意省略 functionMode 测试验证
|
||||
optimizationMode: 'system'
|
||||
})
|
||||
).rejects.toThrow(FavoriteValidationError);
|
||||
});
|
||||
|
||||
it('应该接受合法的 basic/system 模式', async () => {
|
||||
const id = await manager.addFavorite({
|
||||
title: '测试基础模式',
|
||||
content: '测试内容',
|
||||
tags: [],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
expect(id).toBeTruthy();
|
||||
const favorite = await manager.getFavorite(id);
|
||||
expect(favorite.functionMode).toBe('basic');
|
||||
expect(favorite.optimizationMode).toBe('system');
|
||||
});
|
||||
|
||||
it('应该接受合法的 context/user 模式', async () => {
|
||||
const id = await manager.addFavorite({
|
||||
title: '测试上下文模式',
|
||||
content: '测试内容',
|
||||
tags: [],
|
||||
functionMode: 'context',
|
||||
optimizationMode: 'user'
|
||||
});
|
||||
|
||||
expect(id).toBeTruthy();
|
||||
const favorite = await manager.getFavorite(id);
|
||||
expect(favorite.functionMode).toBe('context');
|
||||
expect(favorite.optimizationMode).toBe('user');
|
||||
});
|
||||
|
||||
it('应该接受合法的 image/text2image 模式', async () => {
|
||||
const id = await manager.addFavorite({
|
||||
title: '测试图像模式',
|
||||
content: '测试内容',
|
||||
tags: [],
|
||||
functionMode: 'image',
|
||||
imageSubMode: 'text2image'
|
||||
});
|
||||
|
||||
expect(id).toBeTruthy();
|
||||
const favorite = await manager.getFavorite(id);
|
||||
expect(favorite.functionMode).toBe('image');
|
||||
expect(favorite.imageSubMode).toBe('text2image');
|
||||
});
|
||||
|
||||
it('应该拒绝 basic 模式缺少 optimizationMode', async () => {
|
||||
await expect(
|
||||
manager.addFavorite({
|
||||
title: '测试',
|
||||
content: '内容',
|
||||
tags: [],
|
||||
functionMode: 'basic'
|
||||
// 缺少 optimizationMode
|
||||
})
|
||||
).rejects.toThrow(FavoriteValidationError);
|
||||
});
|
||||
|
||||
it('应该拒绝 image 模式缺少 imageSubMode', async () => {
|
||||
await expect(
|
||||
manager.addFavorite({
|
||||
title: '测试',
|
||||
content: '内容',
|
||||
tags: [],
|
||||
functionMode: 'image'
|
||||
// 缺少 imageSubMode
|
||||
})
|
||||
).rejects.toThrow(FavoriteValidationError);
|
||||
});
|
||||
|
||||
it('basic 模式包含 imageSubMode 不会报错(字段会被保存但不使用)', async () => {
|
||||
// 注意: 当前实现不验证冲突字段,这是一个已知的设计选择
|
||||
const id = await manager.addFavorite({
|
||||
title: '测试',
|
||||
content: '内容',
|
||||
tags: [],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system',
|
||||
imageSubMode: 'text2image' // 冗余字段,不会报错
|
||||
});
|
||||
|
||||
expect(id).toBeTruthy();
|
||||
const favorite = await manager.getFavorite(id);
|
||||
expect(favorite.functionMode).toBe('basic');
|
||||
expect(favorite.optimizationMode).toBe('system');
|
||||
// imageSubMode 会被保存但在 basic 模式下不使用
|
||||
});
|
||||
|
||||
it('image 模式包含 optimizationMode 不会报错(字段会被保存但不使用)', async () => {
|
||||
// 注意: 当前实现不验证冲突字段,这是一个已知的设计选择
|
||||
const id = await manager.addFavorite({
|
||||
title: '测试',
|
||||
content: '内容',
|
||||
tags: [],
|
||||
functionMode: 'image',
|
||||
imageSubMode: 'text2image',
|
||||
optimizationMode: 'system' // 冗余字段,不会报错
|
||||
});
|
||||
|
||||
expect(id).toBeTruthy();
|
||||
const favorite = await manager.getFavorite(id);
|
||||
expect(favorite.functionMode).toBe('image');
|
||||
expect(favorite.imageSubMode).toBe('text2image');
|
||||
// optimizationMode 会被保存但在 image 模式下不使用
|
||||
});
|
||||
});
|
||||
|
||||
describe('addFavorite - metadata 处理', () => {
|
||||
it('应该正确存储 metadata.originalContent', async () => {
|
||||
const id = await manager.addFavorite({
|
||||
title: '测试',
|
||||
content: '优化后内容',
|
||||
tags: [],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system',
|
||||
metadata: {
|
||||
originalContent: '原始内容'
|
||||
}
|
||||
});
|
||||
|
||||
const favorite = await manager.getFavorite(id);
|
||||
expect(favorite.metadata?.originalContent).toBe('原始内容');
|
||||
});
|
||||
|
||||
it('应该正确存储 metadata.sourceHistoryId', async () => {
|
||||
const id = await manager.addFavorite({
|
||||
title: '测试',
|
||||
content: '优化后内容',
|
||||
tags: [],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system',
|
||||
metadata: {
|
||||
sourceHistoryId: 'history-123'
|
||||
}
|
||||
});
|
||||
|
||||
const favorite = await manager.getFavorite(id);
|
||||
expect(favorite.metadata?.sourceHistoryId).toBe('history-123');
|
||||
});
|
||||
|
||||
it('应该保留其他 metadata 字段', async () => {
|
||||
const id = await manager.addFavorite({
|
||||
title: '测试',
|
||||
content: '内容',
|
||||
tags: [],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system',
|
||||
metadata: {
|
||||
customField: '自定义值',
|
||||
anotherField: 123
|
||||
}
|
||||
});
|
||||
|
||||
const favorite = await manager.getFavorite(id);
|
||||
expect(favorite.metadata?.customField).toBe('自定义值');
|
||||
expect(favorite.metadata?.anotherField).toBe(123);
|
||||
});
|
||||
});
|
||||
|
||||
describe('分类管理 - reorderCategories', () => {
|
||||
it('应该能重新排序分类', async () => {
|
||||
// 创建测试分类
|
||||
const cat1Id = await manager.addCategory({ name: '分类1', color: '#ff0000' });
|
||||
const cat2Id = await manager.addCategory({ name: '分类2', color: '#00ff00' });
|
||||
const cat3Id = await manager.addCategory({ name: '分类3', color: '#0000ff' });
|
||||
|
||||
// 重新排序:3, 1, 2
|
||||
await manager.reorderCategories([cat3Id, cat1Id, cat2Id]);
|
||||
|
||||
const categories = await manager.getCategories();
|
||||
|
||||
// 按 sortOrder 排序
|
||||
const sorted = categories.sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
|
||||
expect(sorted[0].id).toBe(cat3Id);
|
||||
expect(sorted[0].sortOrder).toBe(0);
|
||||
|
||||
expect(sorted[1].id).toBe(cat1Id);
|
||||
expect(sorted[1].sortOrder).toBe(1);
|
||||
|
||||
expect(sorted[2].id).toBe(cat2Id);
|
||||
expect(sorted[2].sortOrder).toBe(2);
|
||||
});
|
||||
|
||||
it('应该过滤不存在的分类ID', async () => {
|
||||
const cat1Id = await manager.addCategory({ name: '分类1', color: '#ff0000' });
|
||||
|
||||
// 包含不存在的ID
|
||||
await manager.reorderCategories(['non-existent', cat1Id]);
|
||||
|
||||
const categories = await manager.getCategories();
|
||||
const existingCategory = categories.find(c => c.id === cat1Id);
|
||||
|
||||
expect(existingCategory).toBeDefined();
|
||||
expect(existingCategory!.sortOrder).toBeDefined();
|
||||
});
|
||||
|
||||
it('空数组应该抛出验证错误', async () => {
|
||||
await expect(manager.reorderCategories([])).rejects.toThrow(FavoriteValidationError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('分类管理 - getCategoryUsage', () => {
|
||||
it('应该返回分类的使用次数', async () => {
|
||||
const catId = await manager.addCategory({ name: '测试分类', color: '#ff0000' });
|
||||
|
||||
// 添加3个收藏使用该分类
|
||||
await manager.addFavorite({
|
||||
title: '收藏1',
|
||||
content: '内容1',
|
||||
category: catId,
|
||||
tags: [],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
await manager.addFavorite({
|
||||
title: '收藏2',
|
||||
content: '内容2',
|
||||
category: catId,
|
||||
tags: [],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
await manager.addFavorite({
|
||||
title: '收藏3',
|
||||
content: '内容3',
|
||||
category: catId,
|
||||
tags: [],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
const usage = await manager.getCategoryUsage(catId);
|
||||
expect(usage).toBe(3);
|
||||
});
|
||||
|
||||
it('应该返回0对于未使用的分类', async () => {
|
||||
const catId = await manager.addCategory({ name: '未使用分类', color: '#ff0000' });
|
||||
|
||||
const usage = await manager.getCategoryUsage(catId);
|
||||
expect(usage).toBe(0);
|
||||
});
|
||||
|
||||
it('应该返回0对于不存在的分类', async () => {
|
||||
const usage = await manager.getCategoryUsage('non-existent-id');
|
||||
expect(usage).toBe(0);
|
||||
});
|
||||
|
||||
it('不同分类应该独立计数', async () => {
|
||||
const cat1Id = await manager.addCategory({ name: '分类1', color: '#ff0000' });
|
||||
const cat2Id = await manager.addCategory({ name: '分类2', color: '#00ff00' });
|
||||
|
||||
// 分类1: 2个收藏
|
||||
await manager.addFavorite({
|
||||
title: '收藏1-1',
|
||||
content: '内容',
|
||||
category: cat1Id,
|
||||
tags: [],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
await manager.addFavorite({
|
||||
title: '收藏1-2',
|
||||
content: '内容',
|
||||
category: cat1Id,
|
||||
tags: [],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
// 分类2: 1个收藏
|
||||
await manager.addFavorite({
|
||||
title: '收藏2-1',
|
||||
content: '内容',
|
||||
category: cat2Id,
|
||||
tags: [],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
const usage1 = await manager.getCategoryUsage(cat1Id);
|
||||
const usage2 = await manager.getCategoryUsage(cat2Id);
|
||||
|
||||
expect(usage1).toBe(2);
|
||||
expect(usage2).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('分类管理 - deleteCategory', () => {
|
||||
it('删除分类时应清空收藏的分类字段并返回受影响数量', async () => {
|
||||
const catId = await manager.addCategory({ name: '测试分类', color: '#ff0000' });
|
||||
|
||||
// 添加2个使用该分类的收藏
|
||||
const fav1Id = await manager.addFavorite({
|
||||
title: '收藏1',
|
||||
content: '内容1',
|
||||
category: catId,
|
||||
tags: [],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
const fav2Id = await manager.addFavorite({
|
||||
title: '收藏2',
|
||||
content: '内容2',
|
||||
category: catId,
|
||||
tags: [],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
// 删除分类
|
||||
const affectedCount = await manager.deleteCategory(catId);
|
||||
|
||||
// 应该返回受影响的收藏数量
|
||||
expect(affectedCount).toBe(2);
|
||||
|
||||
// 收藏的分类字段应该被清空
|
||||
const fav1 = await manager.getFavorite(fav1Id);
|
||||
const fav2 = await manager.getFavorite(fav2Id);
|
||||
expect(fav1.category).toBeUndefined();
|
||||
expect(fav2.category).toBeUndefined();
|
||||
|
||||
// 分类应该被删除
|
||||
const categories = await manager.getCategories();
|
||||
expect(categories.find(c => c.id === catId)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('删除空分类应该返回0', async () => {
|
||||
const catId = await manager.addCategory({ name: '空分类', color: '#ff0000' });
|
||||
|
||||
const affectedCount = await manager.deleteCategory(catId);
|
||||
|
||||
expect(affectedCount).toBe(0);
|
||||
});
|
||||
|
||||
it('删除不存在的分类应该抛出错误', async () => {
|
||||
await expect(manager.deleteCategory('non-existent-id')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensureDefaultCategories - 默认分类管理', () => {
|
||||
it('首次调用应该创建默认分类', async () => {
|
||||
const customCategories = [
|
||||
{ name: 'Category 1', description: 'Desc 1', color: '#FF0000' },
|
||||
{ name: 'Category 2', description: 'Desc 2', color: '#00FF00' }
|
||||
];
|
||||
|
||||
await manager.ensureDefaultCategories(customCategories);
|
||||
const categories = await manager.getCategories();
|
||||
|
||||
expect(categories.length).toBe(2);
|
||||
expect(categories.find(c => c.name === 'Category 1')).toBeDefined();
|
||||
expect(categories.find(c => c.name === 'Category 2')).toBeDefined();
|
||||
});
|
||||
|
||||
it('用户删除所有分类后不应自动重建', async () => {
|
||||
// 首次创建
|
||||
await manager.ensureDefaultCategories([
|
||||
{ name: 'Cat1', color: '#FF0000' }
|
||||
]);
|
||||
|
||||
let categories = await manager.getCategories();
|
||||
expect(categories.length).toBe(1);
|
||||
|
||||
// 用户删除所有分类
|
||||
for (const cat of categories) {
|
||||
await manager.deleteCategory(cat.id);
|
||||
}
|
||||
|
||||
categories = await manager.getCategories();
|
||||
expect(categories.length).toBe(0);
|
||||
|
||||
// 再次调用不应创建(已标记为已初始化)
|
||||
await manager.ensureDefaultCategories([
|
||||
{ name: 'Cat1', color: '#FF0000' }
|
||||
]);
|
||||
|
||||
categories = await manager.getCategories();
|
||||
expect(categories.length).toBe(0); // 仍然为0
|
||||
});
|
||||
|
||||
it('已有分类时不应重复创建', async () => {
|
||||
// 先手动创建一个分类
|
||||
await manager.addCategory({ name: 'Existing', color: '#000000' });
|
||||
|
||||
// 调用 ensureDefaultCategories 不应创建新分类
|
||||
await manager.ensureDefaultCategories([
|
||||
{ name: 'New Category', color: '#FFFFFF' }
|
||||
]);
|
||||
|
||||
const categories = await manager.getCategories();
|
||||
expect(categories.length).toBe(1); // 仍然只有1个
|
||||
expect(categories[0].name).toBe('Existing');
|
||||
});
|
||||
|
||||
it('默认分类应该有正确的 sortOrder', async () => {
|
||||
await manager.ensureDefaultCategories([
|
||||
{ name: 'Cat1', color: '#FF0000' },
|
||||
{ name: 'Cat2', color: '#00FF00' },
|
||||
{ name: 'Cat3', color: '#0000FF' }
|
||||
]);
|
||||
|
||||
const categories = await manager.getCategories();
|
||||
const sorted = categories.sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
|
||||
// sortOrder 应该是连续递增的
|
||||
sorted.forEach((cat, index) => {
|
||||
expect(cat.sortOrder).toBe(index);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('TypeMapper 集成测试', () => {
|
||||
it('TypeMapper 映射的结果应该能通过 addFavorite 验证', () => {
|
||||
const testTypes = [
|
||||
'optimize',
|
||||
'userOptimize',
|
||||
'contextSystemOptimize',
|
||||
'contextUserOptimize',
|
||||
'imageOptimize',
|
||||
'text2imageOptimize',
|
||||
'image2imageOptimize'
|
||||
] as const;
|
||||
|
||||
testTypes.forEach(type => {
|
||||
const mapping = TypeMapper.mapFromRecordType(type);
|
||||
// 验证映射结果的有效性
|
||||
expect(TypeMapper.validateMapping(mapping)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('应该能使用 TypeMapper 映射结果创建收藏', async () => {
|
||||
const mapping = TypeMapper.mapFromRecordType('optimize');
|
||||
|
||||
const id = await manager.addFavorite({
|
||||
title: '从TypeMapper映射创建',
|
||||
content: '测试内容',
|
||||
tags: [],
|
||||
...mapping
|
||||
});
|
||||
|
||||
expect(id).toBeTruthy();
|
||||
|
||||
const favorite = await manager.getFavorite(id);
|
||||
expect(favorite.functionMode).toBe(mapping.functionMode);
|
||||
expect(favorite.optimizationMode).toBe(mapping.optimizationMode);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,451 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { FavoriteManager } from '../../../src/services/favorite/manager';
|
||||
import type { IStorageProvider } from '../../../src/services/storage/types';
|
||||
import { FavoriteValidationError, FavoriteError } from '../../../src/services/favorite/errors';
|
||||
|
||||
/**
|
||||
* 标签管理功能单元测试
|
||||
*/
|
||||
describe('FavoriteManager - 标签管理', () => {
|
||||
let manager: FavoriteManager;
|
||||
let mockStorage: Map<string, string>;
|
||||
let storageProvider: IStorageProvider;
|
||||
|
||||
beforeEach(() => {
|
||||
// 创建模拟存储
|
||||
mockStorage = new Map<string, string>();
|
||||
|
||||
storageProvider = {
|
||||
getItem: vi.fn(async (key: string) => mockStorage.get(key) || null),
|
||||
setItem: vi.fn(async (key: string, value: string) => {
|
||||
mockStorage.set(key, value);
|
||||
}),
|
||||
removeItem: vi.fn(async (key: string) => {
|
||||
mockStorage.delete(key);
|
||||
}),
|
||||
clearAll: vi.fn(async () => {
|
||||
mockStorage.clear();
|
||||
}),
|
||||
batchUpdate: vi.fn(async (operations: Array<{ key: string; operation: 'set' | 'remove'; value?: string }>) => {
|
||||
operations.forEach(({ key, operation, value }) => {
|
||||
if (operation === 'set' && value) {
|
||||
mockStorage.set(key, value);
|
||||
} else if (operation === 'remove') {
|
||||
mockStorage.delete(key);
|
||||
}
|
||||
});
|
||||
}),
|
||||
updateData: vi.fn(async (key: string, updater: (data: any) => any) => {
|
||||
const currentData = mockStorage.get(key);
|
||||
const parsedData = currentData ? JSON.parse(currentData) : null;
|
||||
const updatedData = updater(parsedData);
|
||||
mockStorage.set(key, JSON.stringify(updatedData));
|
||||
})
|
||||
};
|
||||
|
||||
manager = new FavoriteManager(storageProvider);
|
||||
});
|
||||
|
||||
describe('添加标签', () => {
|
||||
it('应该能成功添加新标签', async () => {
|
||||
await manager.addTag('测试标签');
|
||||
|
||||
const tags = await manager.getAllTags();
|
||||
expect(tags).toHaveLength(1);
|
||||
expect(tags[0].tag).toBe('测试标签');
|
||||
expect(tags[0].count).toBe(0); // 新标签未使用,count 为 0
|
||||
});
|
||||
|
||||
it('应该拒绝空标签名', async () => {
|
||||
await expect(manager.addTag('')).rejects.toThrow(FavoriteValidationError);
|
||||
await expect(manager.addTag(' ')).rejects.toThrow(FavoriteValidationError);
|
||||
});
|
||||
|
||||
it('应该拒绝重复的标签', async () => {
|
||||
await manager.addTag('标签1');
|
||||
await expect(manager.addTag('标签1')).rejects.toThrow(FavoriteError);
|
||||
});
|
||||
|
||||
it('应该自动去除首尾空格', async () => {
|
||||
await manager.addTag(' 标签2 ');
|
||||
const tags = await manager.getAllTags();
|
||||
expect(tags[0].tag).toBe('标签2');
|
||||
});
|
||||
|
||||
it('应该能添加多个标签', async () => {
|
||||
await manager.addTag('标签1');
|
||||
await manager.addTag('标签2');
|
||||
await manager.addTag('标签3');
|
||||
|
||||
const tags = await manager.getAllTags();
|
||||
expect(tags).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('获取所有标签', () => {
|
||||
it('应该返回独立标签和使用中的标签', async () => {
|
||||
// 添加独立标签
|
||||
await manager.addTag('独立标签1');
|
||||
await manager.addTag('独立标签2');
|
||||
|
||||
// 添加收藏(使用某些标签)
|
||||
await manager.addFavorite({
|
||||
title: '测试收藏',
|
||||
content: '测试内容',
|
||||
tags: ['使用中标签', '独立标签1'],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
const tags = await manager.getAllTags();
|
||||
|
||||
// 应该包含 3 个标签:独立标签1(count=1)、独立标签2(count=0)、使用中标签(count=1)
|
||||
expect(tags).toHaveLength(3);
|
||||
|
||||
const tag1 = tags.find(t => t.tag === '独立标签1');
|
||||
expect(tag1?.count).toBe(1);
|
||||
|
||||
const tag2 = tags.find(t => t.tag === '独立标签2');
|
||||
expect(tag2?.count).toBe(0);
|
||||
|
||||
const tag3 = tags.find(t => t.tag === '使用中标签');
|
||||
expect(tag3?.count).toBe(1);
|
||||
});
|
||||
|
||||
it('应该按使用次数降序排序', async () => {
|
||||
await manager.addFavorite({
|
||||
title: '收藏1',
|
||||
content: '内容1',
|
||||
tags: ['标签A', '标签B'],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
await manager.addFavorite({
|
||||
title: '收藏2',
|
||||
content: '内容2',
|
||||
tags: ['标签B', '标签C'],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
await manager.addFavorite({
|
||||
title: '收藏3',
|
||||
content: '内容3',
|
||||
tags: ['标签B'],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
const tags = await manager.getAllTags();
|
||||
|
||||
// 标签B 使用3次,应该排第一
|
||||
expect(tags[0].tag).toBe('标签B');
|
||||
expect(tags[0].count).toBe(3);
|
||||
|
||||
// 标签A 和 标签C 各使用1次
|
||||
expect(tags[1].count).toBe(1);
|
||||
expect(tags[2].count).toBe(1);
|
||||
});
|
||||
|
||||
it('相同使用次数时应该按标签名升序排序', async () => {
|
||||
await manager.addFavorite({
|
||||
title: '收藏',
|
||||
content: '内容',
|
||||
tags: ['Zebra', 'Apple', 'Banana'],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
const tags = await manager.getAllTags();
|
||||
|
||||
// 都是使用1次,应该按字母顺序排序
|
||||
expect(tags[0].tag).toBe('Apple');
|
||||
expect(tags[1].tag).toBe('Banana');
|
||||
expect(tags[2].tag).toBe('Zebra');
|
||||
});
|
||||
});
|
||||
|
||||
describe('重命名标签', () => {
|
||||
it('应该能成功重命名标签并更新所有收藏', async () => {
|
||||
// 添加使用该标签的收藏
|
||||
await manager.addFavorite({
|
||||
title: '收藏1',
|
||||
content: '内容1',
|
||||
tags: ['旧标签'],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
await manager.addFavorite({
|
||||
title: '收藏2',
|
||||
content: '内容2',
|
||||
tags: ['旧标签', '其他标签'],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
const affectedCount = await manager.renameTag('旧标签', '新标签');
|
||||
|
||||
expect(affectedCount).toBe(2); // 影响了2个收藏
|
||||
|
||||
// 验证收藏已更新
|
||||
const favorites = await manager.getFavorites();
|
||||
expect(favorites[0].tags).toContain('新标签');
|
||||
expect(favorites[0].tags).not.toContain('旧标签');
|
||||
expect(favorites[1].tags).toContain('新标签');
|
||||
expect(favorites[1].tags).not.toContain('旧标签');
|
||||
|
||||
// 验证标签统计
|
||||
const tags = await manager.getAllTags();
|
||||
const newTag = tags.find(t => t.tag === '新标签');
|
||||
expect(newTag?.count).toBe(2);
|
||||
|
||||
const oldTag = tags.find(t => t.tag === '旧标签');
|
||||
expect(oldTag).toBeUndefined();
|
||||
});
|
||||
|
||||
it('重命名为相同名称时应该返回0', async () => {
|
||||
const affectedCount = await manager.renameTag('标签A', '标签A');
|
||||
expect(affectedCount).toBe(0);
|
||||
});
|
||||
|
||||
it('应该拒绝空标签名', async () => {
|
||||
await expect(manager.renameTag('', '新标签')).rejects.toThrow(FavoriteValidationError);
|
||||
await expect(manager.renameTag('旧标签', '')).rejects.toThrow(FavoriteValidationError);
|
||||
});
|
||||
|
||||
it('重命名不存在的标签时应该返回0', async () => {
|
||||
const affectedCount = await manager.renameTag('不存在的标签', '新标签');
|
||||
expect(affectedCount).toBe(0);
|
||||
});
|
||||
|
||||
it('重命名时如果新标签已存在应该合并', async () => {
|
||||
await manager.addFavorite({
|
||||
title: '收藏',
|
||||
content: '内容',
|
||||
tags: ['标签A', '标签B'],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
await manager.renameTag('标签A', '标签B');
|
||||
|
||||
const favorites = await manager.getFavorites();
|
||||
// 应该只包含一个标签B,不重复
|
||||
expect(favorites[0].tags).toEqual(['标签B']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('合并标签', () => {
|
||||
it('应该能成功合并多个标签', async () => {
|
||||
await manager.addFavorite({
|
||||
title: '收藏1',
|
||||
content: '内容1',
|
||||
tags: ['标签A'],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
await manager.addFavorite({
|
||||
title: '收藏2',
|
||||
content: '内容2',
|
||||
tags: ['标签B'],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
await manager.addFavorite({
|
||||
title: '收藏3',
|
||||
content: '内容3',
|
||||
tags: ['标签C'],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
const affectedCount = await manager.mergeTags(['标签A', '标签B'], '标签C');
|
||||
|
||||
expect(affectedCount).toBe(2); // 影响了2个收藏(收藏1和收藏2)
|
||||
|
||||
const favorites = await manager.getFavorites();
|
||||
expect(favorites[0].tags).toContain('标签C');
|
||||
expect(favorites[0].tags).not.toContain('标签A');
|
||||
expect(favorites[1].tags).toContain('标签C');
|
||||
expect(favorites[1].tags).not.toContain('标签B');
|
||||
|
||||
// 收藏3的标签C不应重复
|
||||
expect(favorites[2].tags).toEqual(['标签C']);
|
||||
});
|
||||
|
||||
it('应该拒绝空的源标签列表', async () => {
|
||||
await expect(manager.mergeTags([], '目标标签')).rejects.toThrow(FavoriteValidationError);
|
||||
});
|
||||
|
||||
it('应该拒绝空的目标标签', async () => {
|
||||
await expect(manager.mergeTags(['标签A'], '')).rejects.toThrow(FavoriteValidationError);
|
||||
});
|
||||
|
||||
it('合并不存在的标签时应该返回0', async () => {
|
||||
const affectedCount = await manager.mergeTags(['不存在1', '不存在2'], '目标');
|
||||
expect(affectedCount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('删除标签', () => {
|
||||
it('应该能删除标签并从所有收藏中移除', async () => {
|
||||
// 添加独立标签(未被使用)
|
||||
await manager.addTag('独立标签');
|
||||
|
||||
// 添加使用该标签的收藏
|
||||
await manager.addFavorite({
|
||||
title: '收藏1',
|
||||
content: '内容1',
|
||||
tags: ['要删除的标签', '保留的标签'],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
await manager.addFavorite({
|
||||
title: '收藏2',
|
||||
content: '内容2',
|
||||
tags: ['要删除的标签'],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
const affectedCount = await manager.deleteTag('要删除的标签');
|
||||
|
||||
expect(affectedCount).toBe(2); // 影响了2个收藏
|
||||
|
||||
// 验证收藏已更新
|
||||
const favorites = await manager.getFavorites();
|
||||
|
||||
// 验证两个收藏都不包含"要删除的标签"
|
||||
favorites.forEach(fav => {
|
||||
expect(fav.tags).not.toContain('要删除的标签');
|
||||
});
|
||||
|
||||
// 第一个收藏应该保留"保留的标签"
|
||||
const firstFavorite = favorites.find(f => f.title === '收藏1');
|
||||
expect(firstFavorite).toBeDefined();
|
||||
expect(firstFavorite!.tags).toEqual(['保留的标签']);
|
||||
|
||||
// 第二个收藏应该没有标签
|
||||
const secondFavorite = favorites.find(f => f.title === '收藏2');
|
||||
expect(secondFavorite).toBeDefined();
|
||||
expect(secondFavorite!.tags).toEqual([]);
|
||||
|
||||
// 验证标签统计:要删除的标签不应该存在
|
||||
const tags = await manager.getAllTags();
|
||||
const deletedTag = tags.find(t => t.tag === '要删除的标签');
|
||||
expect(deletedTag).toBeUndefined();
|
||||
|
||||
// 独立标签应该仍然存在(count=0,因为未被使用)
|
||||
const independentTag = tags.find(t => t.tag === '独立标签');
|
||||
expect(independentTag).toBeDefined();
|
||||
expect(independentTag!.count).toBe(0);
|
||||
|
||||
// 测试删除独立标签
|
||||
const independentTagDelCount = await manager.deleteTag('独立标签');
|
||||
expect(independentTagDelCount).toBe(0); // 未被任何收藏使用
|
||||
|
||||
const tagsAfterDel = await manager.getAllTags();
|
||||
const deletedIndependentTag = tagsAfterDel.find(t => t.tag === '独立标签');
|
||||
expect(deletedIndependentTag).toBeUndefined();
|
||||
});
|
||||
|
||||
it('应该拒绝空标签名', async () => {
|
||||
await expect(manager.deleteTag('')).rejects.toThrow(FavoriteValidationError);
|
||||
});
|
||||
|
||||
it('删除不存在的标签时应该返回0', async () => {
|
||||
const affectedCount = await manager.deleteTag('不存在的标签');
|
||||
expect(affectedCount).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('标签导入导出', () => {
|
||||
it('导出时应该包含独立标签', async () => {
|
||||
await manager.addTag('独立标签1');
|
||||
await manager.addTag('独立标签2');
|
||||
|
||||
// 添加使用中标签到独立标签库
|
||||
await manager.addTag('使用中标签');
|
||||
|
||||
await manager.addFavorite({
|
||||
title: '测试',
|
||||
content: '内容',
|
||||
tags: ['使用中标签'],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
const exportData = await manager.exportFavorites();
|
||||
const parsed = JSON.parse(exportData);
|
||||
|
||||
expect(parsed.tags).toBeDefined();
|
||||
expect(parsed.tags).toContain('独立标签1');
|
||||
expect(parsed.tags).toContain('独立标签2');
|
||||
expect(parsed.tags).toContain('使用中标签');
|
||||
});
|
||||
|
||||
it('导入时应该自动创建独立标签', async () => {
|
||||
const importData = JSON.stringify({
|
||||
version: '1.0',
|
||||
exportDate: new Date().toISOString(),
|
||||
favorites: [
|
||||
{
|
||||
title: '测试',
|
||||
content: '内容',
|
||||
tags: ['标签1', '标签2'],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
}
|
||||
],
|
||||
categories: [],
|
||||
tags: ['标签1', '标签2', '预创建标签']
|
||||
});
|
||||
|
||||
await manager.importFavorites(importData);
|
||||
|
||||
const tags = await manager.getAllTags();
|
||||
|
||||
// 应该包含所有标签
|
||||
expect(tags.find(t => t.tag === '标签1')).toBeDefined();
|
||||
expect(tags.find(t => t.tag === '标签2')).toBeDefined();
|
||||
expect(tags.find(t => t.tag === '预创建标签')).toBeDefined();
|
||||
|
||||
// 预创建标签使用次数为0
|
||||
const preCreatedTag = tags.find(t => t.tag === '预创建标签');
|
||||
expect(preCreatedTag?.count).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('保存收藏时自动注册标签', () => {
|
||||
it('保存收藏时应该自动将标签添加到独立标签库', async () => {
|
||||
// 这个测试需要在 UI 层面实现,这里只是验证概念
|
||||
// 在 SaveFavoriteDialog 中,保存前应该调用 addTag
|
||||
|
||||
await manager.addFavorite({
|
||||
title: '测试',
|
||||
content: '内容',
|
||||
tags: ['新标签1', '新标签2'],
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
|
||||
// 手动模拟 SaveFavoriteDialog 的行为
|
||||
for (const tag of ['新标签1', '新标签2']) {
|
||||
try {
|
||||
await manager.addTag(tag);
|
||||
} catch (error) {
|
||||
// 标签已存在,忽略错误
|
||||
}
|
||||
}
|
||||
|
||||
const tags = await manager.getAllTags();
|
||||
expect(tags.find(t => t.tag === '新标签1')).toBeDefined();
|
||||
expect(tags.find(t => t.tag === '新标签2')).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,218 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TagTypeConverter } from '../../../src/services/favorite/type-converter';
|
||||
import type { TagStatistics } from '../../../src/services/favorite/types';
|
||||
|
||||
/**
|
||||
* 标签类型转换器单元测试
|
||||
*/
|
||||
describe('TagTypeConverter', () => {
|
||||
describe('toTagStatistics', () => {
|
||||
it('应该将API格式转换为TagStatistics格式', () => {
|
||||
const apiData = [
|
||||
{ tag: '标签1', count: 5 },
|
||||
{ tag: '标签2', count: 3 }
|
||||
];
|
||||
|
||||
const result = TagTypeConverter.toTagStatistics(apiData);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toEqual({
|
||||
name: '标签1',
|
||||
count: 5,
|
||||
lastUsed: undefined
|
||||
});
|
||||
expect(result[1]).toEqual({
|
||||
name: '标签2',
|
||||
count: 3,
|
||||
lastUsed: undefined
|
||||
});
|
||||
});
|
||||
|
||||
it('应该处理空数组', () => {
|
||||
const result = TagTypeConverter.toTagStatistics([]);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fromTagStatistics', () => {
|
||||
it('应该将TagStatistics格式转换为API格式', () => {
|
||||
const stats: TagStatistics[] = [
|
||||
{ name: '标签1', count: 5, lastUsed: undefined },
|
||||
{ name: '标签2', count: 3, lastUsed: Date.now() }
|
||||
];
|
||||
|
||||
const result = TagTypeConverter.fromTagStatistics(stats);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toEqual({ tag: '标签1', count: 5 });
|
||||
expect(result[1]).toEqual({ tag: '标签2', count: 3 });
|
||||
});
|
||||
|
||||
it('应该处理空数组', () => {
|
||||
const result = TagTypeConverter.fromTagStatistics([]);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toAutoCompleteOptions', () => {
|
||||
it('应该转换为自动完成选项格式', () => {
|
||||
const apiData = [
|
||||
{ tag: '标签1', count: 5 },
|
||||
{ tag: '标签2', count: 3 }
|
||||
];
|
||||
|
||||
const result = TagTypeConverter.toAutoCompleteOptions(apiData);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toEqual({
|
||||
label: '标签1 (5)',
|
||||
value: '标签1',
|
||||
count: 5
|
||||
});
|
||||
expect(result[1]).toEqual({
|
||||
label: '标签2 (3)',
|
||||
value: '标签2',
|
||||
count: 3
|
||||
});
|
||||
});
|
||||
|
||||
it('应该处理使用次数为0的标签', () => {
|
||||
const apiData = [{ tag: '未使用标签', count: 0 }];
|
||||
|
||||
const result = TagTypeConverter.toAutoCompleteOptions(apiData);
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
label: '未使用标签 (0)',
|
||||
value: '未使用标签',
|
||||
count: 0
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('toStringArray', () => {
|
||||
it('应该转换为字符串数组', () => {
|
||||
const apiData = [
|
||||
{ tag: '标签1', count: 5 },
|
||||
{ tag: '标签2', count: 3 },
|
||||
{ tag: '标签3', count: 0 }
|
||||
];
|
||||
|
||||
const result = TagTypeConverter.toStringArray(apiData);
|
||||
|
||||
expect(result).toEqual(['标签1', '标签2', '标签3']);
|
||||
});
|
||||
|
||||
it('应该处理空数组', () => {
|
||||
const result = TagTypeConverter.toStringArray([]);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sortByCount', () => {
|
||||
it('应该按使用次数降序排序', () => {
|
||||
const tags = [
|
||||
{ count: 3 },
|
||||
{ count: 10 },
|
||||
{ count: 1 },
|
||||
{ count: 5 }
|
||||
];
|
||||
|
||||
const result = TagTypeConverter.sortByCount(tags);
|
||||
|
||||
expect(result.map(t => t.count)).toEqual([10, 5, 3, 1]);
|
||||
});
|
||||
|
||||
it('应该不修改原数组', () => {
|
||||
const tags = [
|
||||
{ count: 3 },
|
||||
{ count: 1 }
|
||||
];
|
||||
|
||||
const result = TagTypeConverter.sortByCount(tags);
|
||||
|
||||
expect(tags.map(t => t.count)).toEqual([3, 1]);
|
||||
expect(result.map(t => t.count)).toEqual([3, 1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sortByName', () => {
|
||||
it('应该按标签名升序排序', () => {
|
||||
const tags: TagStatistics[] = [
|
||||
{ name: 'Zebra', count: 1 },
|
||||
{ name: 'Apple', count: 2 },
|
||||
{ name: 'Banana', count: 3 }
|
||||
];
|
||||
|
||||
const result = TagTypeConverter.sortByName(tags);
|
||||
|
||||
expect(result.map(t => t.name)).toEqual(['Apple', 'Banana', 'Zebra']);
|
||||
});
|
||||
|
||||
it('应该正确处理中文排序', () => {
|
||||
const tags: TagStatistics[] = [
|
||||
{ name: '编程', count: 1 },
|
||||
{ name: '设计', count: 2 },
|
||||
{ name: '测试', count: 3 }
|
||||
];
|
||||
|
||||
const result = TagTypeConverter.sortByName(tags);
|
||||
|
||||
// 中文按拼音排序
|
||||
expect(result.map(t => t.name)).toEqual(['编程', '测试', '设计']);
|
||||
});
|
||||
|
||||
it('应该不修改原数组', () => {
|
||||
const tags: TagStatistics[] = [
|
||||
{ name: 'B', count: 1 },
|
||||
{ name: 'A', count: 2 }
|
||||
];
|
||||
|
||||
const result = TagTypeConverter.sortByName(tags);
|
||||
|
||||
expect(tags.map(t => t.name)).toEqual(['B', 'A']);
|
||||
expect(result.map(t => t.name)).toEqual(['A', 'B']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sortByCountThenName', () => {
|
||||
it('应该先按使用次数降序,再按名称升序排序', () => {
|
||||
const tags: TagStatistics[] = [
|
||||
{ name: 'Zebra', count: 5 },
|
||||
{ name: 'Apple', count: 5 },
|
||||
{ name: 'Banana', count: 3 },
|
||||
{ name: 'Cat', count: 5 }
|
||||
];
|
||||
|
||||
const result = TagTypeConverter.sortByCountThenName(tags);
|
||||
|
||||
// count=5的按名称升序: Apple, Cat, Zebra
|
||||
// count=3的: Banana
|
||||
expect(result.map(t => t.name)).toEqual(['Apple', 'Cat', 'Zebra', 'Banana']);
|
||||
});
|
||||
|
||||
it('应该处理使用次数全部相同的情况', () => {
|
||||
const tags: TagStatistics[] = [
|
||||
{ name: 'C', count: 1 },
|
||||
{ name: 'A', count: 1 },
|
||||
{ name: 'B', count: 1 }
|
||||
];
|
||||
|
||||
const result = TagTypeConverter.sortByCountThenName(tags);
|
||||
|
||||
expect(result.map(t => t.name)).toEqual(['A', 'B', 'C']);
|
||||
});
|
||||
|
||||
it('应该不修改原数组', () => {
|
||||
const tags: TagStatistics[] = [
|
||||
{ name: 'B', count: 2 },
|
||||
{ name: 'A', count: 1 }
|
||||
];
|
||||
|
||||
const result = TagTypeConverter.sortByCountThenName(tags);
|
||||
|
||||
expect(tags[0].name).toBe('B');
|
||||
expect(result[0].name).toBe('B');
|
||||
expect(result[1].name).toBe('A');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,397 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TypeMapper, type FunctionModeMapping } from '../../../src/services/favorite/type-mapper';
|
||||
import type { PromptRecordType } from '../../../src/services/history/types';
|
||||
|
||||
/**
|
||||
* TypeMapper 单元测试
|
||||
* 测试历史记录类型到功能模式的映射逻辑
|
||||
*/
|
||||
describe('TypeMapper', () => {
|
||||
describe('mapFromRecordType - 基础模式映射', () => {
|
||||
it('应该将 optimize 映射为 basic/system', () => {
|
||||
const result = TypeMapper.mapFromRecordType('optimize');
|
||||
expect(result).toEqual({
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
});
|
||||
|
||||
it('应该将 iterate 映射为 basic/system', () => {
|
||||
const result = TypeMapper.mapFromRecordType('iterate');
|
||||
expect(result).toEqual({
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
});
|
||||
|
||||
it('应该将 userOptimize 映射为 basic/user', () => {
|
||||
const result = TypeMapper.mapFromRecordType('userOptimize');
|
||||
expect(result).toEqual({
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'user'
|
||||
});
|
||||
});
|
||||
|
||||
it('应该将 test 映射为 basic/system', () => {
|
||||
const result = TypeMapper.mapFromRecordType('test');
|
||||
expect(result).toEqual({
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapFromRecordType - 上下文模式映射', () => {
|
||||
it('应该将 contextSystemOptimize 映射为 context/system', () => {
|
||||
const result = TypeMapper.mapFromRecordType('contextSystemOptimize');
|
||||
expect(result).toEqual({
|
||||
functionMode: 'context',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
});
|
||||
|
||||
it('应该将 contextIterate 映射为 context/system', () => {
|
||||
const result = TypeMapper.mapFromRecordType('contextIterate');
|
||||
expect(result).toEqual({
|
||||
functionMode: 'context',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
});
|
||||
|
||||
it('应该将 contextUserOptimize 映射为 context/user', () => {
|
||||
const result = TypeMapper.mapFromRecordType('contextUserOptimize');
|
||||
expect(result).toEqual({
|
||||
functionMode: 'context',
|
||||
optimizationMode: 'user'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapFromRecordType - 图像模式映射', () => {
|
||||
it('应该将 imageOptimize 映射为 image/text2image', () => {
|
||||
const result = TypeMapper.mapFromRecordType('imageOptimize');
|
||||
expect(result).toEqual({
|
||||
functionMode: 'image',
|
||||
imageSubMode: 'text2image'
|
||||
});
|
||||
});
|
||||
|
||||
it('应该将 contextImageOptimize 映射为 image/text2image', () => {
|
||||
const result = TypeMapper.mapFromRecordType('contextImageOptimize');
|
||||
expect(result).toEqual({
|
||||
functionMode: 'image',
|
||||
imageSubMode: 'text2image'
|
||||
});
|
||||
});
|
||||
|
||||
it('应该将 imageIterate 映射为 image/text2image', () => {
|
||||
const result = TypeMapper.mapFromRecordType('imageIterate');
|
||||
expect(result).toEqual({
|
||||
functionMode: 'image',
|
||||
imageSubMode: 'text2image'
|
||||
});
|
||||
});
|
||||
|
||||
it('应该将 text2imageOptimize 映射为 image/text2image', () => {
|
||||
const result = TypeMapper.mapFromRecordType('text2imageOptimize');
|
||||
expect(result).toEqual({
|
||||
functionMode: 'image',
|
||||
imageSubMode: 'text2image'
|
||||
});
|
||||
});
|
||||
|
||||
it('应该将 image2imageOptimize 映射为 image/image2image', () => {
|
||||
const result = TypeMapper.mapFromRecordType('image2imageOptimize');
|
||||
expect(result).toEqual({
|
||||
functionMode: 'image',
|
||||
imageSubMode: 'image2image'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapFromRecordType - 未知类型处理', () => {
|
||||
it('应该将未知类型映射为 basic/system 并输出警告', () => {
|
||||
// 使用 console.warn 的 spy
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
const result = TypeMapper.mapFromRecordType('unknown' as PromptRecordType);
|
||||
|
||||
expect(result).toEqual({
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
});
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'[TypeMapper] Unknown record type: unknown, falling back to basic/system'
|
||||
);
|
||||
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateMapping - 合法映射验证', () => {
|
||||
it('应该接受合法的 basic/system 映射', () => {
|
||||
const mapping: FunctionModeMapping = {
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
};
|
||||
expect(TypeMapper.validateMapping(mapping)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该接受合法的 basic/user 映射', () => {
|
||||
const mapping: FunctionModeMapping = {
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'user'
|
||||
};
|
||||
expect(TypeMapper.validateMapping(mapping)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该接受合法的 context/system 映射', () => {
|
||||
const mapping: FunctionModeMapping = {
|
||||
functionMode: 'context',
|
||||
optimizationMode: 'system'
|
||||
};
|
||||
expect(TypeMapper.validateMapping(mapping)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该接受合法的 context/user 映射', () => {
|
||||
const mapping: FunctionModeMapping = {
|
||||
functionMode: 'context',
|
||||
optimizationMode: 'user'
|
||||
};
|
||||
expect(TypeMapper.validateMapping(mapping)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该接受合法的 image/text2image 映射', () => {
|
||||
const mapping: FunctionModeMapping = {
|
||||
functionMode: 'image',
|
||||
imageSubMode: 'text2image'
|
||||
};
|
||||
expect(TypeMapper.validateMapping(mapping)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该接受合法的 image/image2image 映射', () => {
|
||||
const mapping: FunctionModeMapping = {
|
||||
functionMode: 'image',
|
||||
imageSubMode: 'image2image'
|
||||
};
|
||||
expect(TypeMapper.validateMapping(mapping)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateMapping - 非法映射验证', () => {
|
||||
it('应该拒绝缺少 functionMode 的映射', () => {
|
||||
const mapping: Partial<FunctionModeMapping> = {
|
||||
optimizationMode: 'system'
|
||||
};
|
||||
expect(TypeMapper.validateMapping(mapping)).toBe(false);
|
||||
});
|
||||
|
||||
it('应该拒绝非法的 functionMode 值', () => {
|
||||
const mapping = {
|
||||
functionMode: 'invalid' as any,
|
||||
optimizationMode: 'system'
|
||||
};
|
||||
expect(TypeMapper.validateMapping(mapping)).toBe(false);
|
||||
});
|
||||
|
||||
it('应该拒绝 basic 模式缺少 optimizationMode', () => {
|
||||
const mapping: Partial<FunctionModeMapping> = {
|
||||
functionMode: 'basic'
|
||||
};
|
||||
expect(TypeMapper.validateMapping(mapping)).toBe(false);
|
||||
});
|
||||
|
||||
it('应该拒绝 context 模式缺少 optimizationMode', () => {
|
||||
const mapping: Partial<FunctionModeMapping> = {
|
||||
functionMode: 'context'
|
||||
};
|
||||
expect(TypeMapper.validateMapping(mapping)).toBe(false);
|
||||
});
|
||||
|
||||
it('应该拒绝 basic 模式的非法 optimizationMode 值', () => {
|
||||
const mapping = {
|
||||
functionMode: 'basic' as const,
|
||||
optimizationMode: 'invalid' as any
|
||||
};
|
||||
expect(TypeMapper.validateMapping(mapping)).toBe(false);
|
||||
});
|
||||
|
||||
it('应该拒绝 basic 模式包含 imageSubMode', () => {
|
||||
const mapping = {
|
||||
functionMode: 'basic' as const,
|
||||
optimizationMode: 'system' as const,
|
||||
imageSubMode: 'text2image' as const
|
||||
};
|
||||
expect(TypeMapper.validateMapping(mapping)).toBe(false);
|
||||
});
|
||||
|
||||
it('应该拒绝 context 模式包含 imageSubMode', () => {
|
||||
const mapping = {
|
||||
functionMode: 'context' as const,
|
||||
optimizationMode: 'system' as const,
|
||||
imageSubMode: 'text2image' as const
|
||||
};
|
||||
expect(TypeMapper.validateMapping(mapping)).toBe(false);
|
||||
});
|
||||
|
||||
it('应该拒绝 image 模式缺少 imageSubMode', () => {
|
||||
const mapping: Partial<FunctionModeMapping> = {
|
||||
functionMode: 'image'
|
||||
};
|
||||
expect(TypeMapper.validateMapping(mapping)).toBe(false);
|
||||
});
|
||||
|
||||
it('应该拒绝 image 模式的非法 imageSubMode 值', () => {
|
||||
const mapping = {
|
||||
functionMode: 'image' as const,
|
||||
imageSubMode: 'invalid' as any
|
||||
};
|
||||
expect(TypeMapper.validateMapping(mapping)).toBe(false);
|
||||
});
|
||||
|
||||
it('应该拒绝 image 模式包含 optimizationMode', () => {
|
||||
const mapping = {
|
||||
functionMode: 'image' as const,
|
||||
imageSubMode: 'text2image' as const,
|
||||
optimizationMode: 'system' as const
|
||||
};
|
||||
expect(TypeMapper.validateMapping(mapping)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('inferRecordTypes - 反向推断', () => {
|
||||
it('应该从 basic/system 推断出 optimize 和 iterate', () => {
|
||||
const mapping: FunctionModeMapping = {
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'system'
|
||||
};
|
||||
const result = TypeMapper.inferRecordTypes(mapping);
|
||||
expect(result).toEqual(['optimize', 'iterate']);
|
||||
});
|
||||
|
||||
it('应该从 basic/user 推断出 userOptimize', () => {
|
||||
const mapping: FunctionModeMapping = {
|
||||
functionMode: 'basic',
|
||||
optimizationMode: 'user'
|
||||
};
|
||||
const result = TypeMapper.inferRecordTypes(mapping);
|
||||
expect(result).toEqual(['userOptimize']);
|
||||
});
|
||||
|
||||
it('应该从 context/system 推断出 contextSystemOptimize 和 contextIterate', () => {
|
||||
const mapping: FunctionModeMapping = {
|
||||
functionMode: 'context',
|
||||
optimizationMode: 'system'
|
||||
};
|
||||
const result = TypeMapper.inferRecordTypes(mapping);
|
||||
expect(result).toEqual(['contextSystemOptimize', 'contextIterate']);
|
||||
});
|
||||
|
||||
it('应该从 context/user 推断出 contextUserOptimize', () => {
|
||||
const mapping: FunctionModeMapping = {
|
||||
functionMode: 'context',
|
||||
optimizationMode: 'user'
|
||||
};
|
||||
const result = TypeMapper.inferRecordTypes(mapping);
|
||||
expect(result).toEqual(['contextUserOptimize']);
|
||||
});
|
||||
|
||||
it('应该从 image/text2image 推断出所有文生图类型', () => {
|
||||
const mapping: FunctionModeMapping = {
|
||||
functionMode: 'image',
|
||||
imageSubMode: 'text2image'
|
||||
};
|
||||
const result = TypeMapper.inferRecordTypes(mapping);
|
||||
expect(result).toEqual([
|
||||
'imageOptimize',
|
||||
'contextImageOptimize',
|
||||
'imageIterate',
|
||||
'text2imageOptimize'
|
||||
]);
|
||||
});
|
||||
|
||||
it('应该从 image/image2image 推断出 image2imageOptimize', () => {
|
||||
const mapping: FunctionModeMapping = {
|
||||
functionMode: 'image',
|
||||
imageSubMode: 'image2image'
|
||||
};
|
||||
const result = TypeMapper.inferRecordTypes(mapping);
|
||||
expect(result).toEqual(['image2imageOptimize']);
|
||||
});
|
||||
|
||||
it('应该对非法映射返回空数组', () => {
|
||||
const mapping = {
|
||||
functionMode: 'basic' as const,
|
||||
optimizationMode: undefined as any
|
||||
};
|
||||
const result = TypeMapper.inferRecordTypes(mapping);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('映射和验证的完整性测试', () => {
|
||||
it('所有 PromptRecordType 映射后的结果都应该是合法的', () => {
|
||||
const allTypes: PromptRecordType[] = [
|
||||
'optimize',
|
||||
'userOptimize',
|
||||
'iterate',
|
||||
'test',
|
||||
'contextSystemOptimize',
|
||||
'contextUserOptimize',
|
||||
'contextIterate',
|
||||
'imageOptimize',
|
||||
'contextImageOptimize',
|
||||
'imageIterate',
|
||||
'text2imageOptimize',
|
||||
'image2imageOptimize'
|
||||
];
|
||||
|
||||
allTypes.forEach(type => {
|
||||
const mapping = TypeMapper.mapFromRecordType(type);
|
||||
expect(TypeMapper.validateMapping(mapping)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('映射和反向推断应该是一致的', () => {
|
||||
const testCases: Array<{
|
||||
recordType: PromptRecordType;
|
||||
mapping: FunctionModeMapping;
|
||||
}> = [
|
||||
{
|
||||
recordType: 'optimize',
|
||||
mapping: { functionMode: 'basic', optimizationMode: 'system' }
|
||||
},
|
||||
{
|
||||
recordType: 'userOptimize',
|
||||
mapping: { functionMode: 'basic', optimizationMode: 'user' }
|
||||
},
|
||||
{
|
||||
recordType: 'contextSystemOptimize',
|
||||
mapping: { functionMode: 'context', optimizationMode: 'system' }
|
||||
},
|
||||
{
|
||||
recordType: 'contextUserOptimize',
|
||||
mapping: { functionMode: 'context', optimizationMode: 'user' }
|
||||
},
|
||||
{
|
||||
recordType: 'text2imageOptimize',
|
||||
mapping: { functionMode: 'image', imageSubMode: 'text2image' }
|
||||
},
|
||||
{
|
||||
recordType: 'image2imageOptimize',
|
||||
mapping: { functionMode: 'image', imageSubMode: 'image2image' }
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ recordType, mapping }) => {
|
||||
// 正向映射
|
||||
const mappedResult = TypeMapper.mapFromRecordType(recordType);
|
||||
expect(mappedResult).toEqual(mapping);
|
||||
|
||||
// 反向推断
|
||||
const inferredTypes = TypeMapper.inferRecordTypes(mapping);
|
||||
expect(inferredTypes).toContain(recordType);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,19 +14,54 @@
|
||||
<NEllipsis style="flex: 1; min-width: 0; font-weight: 600; font-size: 14px;">
|
||||
{{ favorite.title }}
|
||||
</NEllipsis>
|
||||
<div style="flex-shrink: 0; max-width: 120px; overflow: hidden;">
|
||||
<NSpace :size="4" style="flex-shrink: 1; min-width: 0;" :wrap="false">
|
||||
<!-- 功能模式标签 -->
|
||||
<NTag
|
||||
v-if="category"
|
||||
:color="{ color: category.color, textColor: 'white' }"
|
||||
:type="getFunctionModeTagType(favorite.functionMode)"
|
||||
size="small"
|
||||
:bordered="false"
|
||||
style="max-width: 100%;"
|
||||
style="flex-shrink: 0;"
|
||||
>
|
||||
<NEllipsis style="max-width: 100px;">
|
||||
{{ category.name }}
|
||||
</NEllipsis>
|
||||
{{ t(`favorites.manager.card.functionMode.${favorite.functionMode}`) }}
|
||||
</NTag>
|
||||
</div>
|
||||
|
||||
<!-- 子模式标签 (优化模式或图像模式) -->
|
||||
<NTag
|
||||
v-if="favorite.optimizationMode || favorite.imageSubMode"
|
||||
:type="getSubModeTagType(favorite)"
|
||||
size="small"
|
||||
:bordered="false"
|
||||
style="flex-shrink: 0;"
|
||||
>
|
||||
{{ getSubModeLabel(favorite) }}
|
||||
</NTag>
|
||||
|
||||
<!-- 分类标签:可收缩 -->
|
||||
<NTooltip
|
||||
v-if="category"
|
||||
trigger="hover"
|
||||
:overlay-style="tooltipOverlayStyle"
|
||||
:content-style="tooltipContentStyle"
|
||||
:theme-overrides="tooltipThemeOverrides"
|
||||
placement="bottom"
|
||||
to="body"
|
||||
:flip="true"
|
||||
>
|
||||
<template #trigger>
|
||||
<NTag
|
||||
:color="{ color: category.color, textColor: 'white' }"
|
||||
size="small"
|
||||
:bordered="false"
|
||||
style="max-width: 80px; flex-shrink: 1; min-width: 0;"
|
||||
>
|
||||
<NEllipsis style="max-width: 68px;">
|
||||
{{ category.name }}
|
||||
</NEllipsis>
|
||||
</NTag>
|
||||
</template>
|
||||
{{ category.name }}
|
||||
</NTooltip>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
@@ -492,6 +527,38 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// 功能模式标签类型映射
|
||||
const getFunctionModeTagType = (mode: string): 'default' | 'info' | 'success' => {
|
||||
const typeMap: Record<string, 'default' | 'info' | 'success'> = {
|
||||
basic: 'default',
|
||||
context: 'info',
|
||||
image: 'success'
|
||||
};
|
||||
return typeMap[mode] || 'default';
|
||||
};
|
||||
|
||||
// 子模式标签类型映射
|
||||
const getSubModeTagType = (favorite: FavoritePrompt): 'warning' | 'error' | 'success' | 'info' | 'default' => {
|
||||
if (favorite.optimizationMode) {
|
||||
return favorite.optimizationMode === 'system' ? 'warning' : 'error';
|
||||
}
|
||||
if (favorite.imageSubMode) {
|
||||
return favorite.imageSubMode === 'text2image' ? 'success' : 'info';
|
||||
}
|
||||
return 'default';
|
||||
};
|
||||
|
||||
// 获取子模式标签文本
|
||||
const getSubModeLabel = (favorite: FavoritePrompt): string => {
|
||||
if (favorite.optimizationMode) {
|
||||
return t(`favorites.manager.card.optimizationMode.${favorite.optimizationMode}`);
|
||||
}
|
||||
if (favorite.imageSubMode) {
|
||||
return t(`favorites.manager.card.imageSubMode.${favorite.imageSubMode}`);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const formatDate = (timestamp: number) => {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
|
||||
@@ -222,6 +222,13 @@
|
||||
<CategoryManager @category-updated="handleCategoryUpdated" />
|
||||
</n-modal>
|
||||
|
||||
<!-- 标签管理 -->
|
||||
<TagManager
|
||||
:show="tagManagerVisible"
|
||||
@update:show="tagManagerVisible = $event"
|
||||
@updated="loadFavorites"
|
||||
/>
|
||||
|
||||
<!-- 新建/编辑收藏对话框 -->
|
||||
<SaveFavoriteDialog
|
||||
:show="createState.visible"
|
||||
@@ -261,6 +268,7 @@ import {
|
||||
} from 'naive-ui';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useToast } from '../composables/useToast';
|
||||
import { useFavoriteInitializer } from '../composables/useFavoriteInitializer';
|
||||
import ToastUI from './Toast.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
@@ -269,13 +277,16 @@ import OutputDisplayFullscreen from './OutputDisplayFullscreen.vue';
|
||||
import CategoryManager from './CategoryManager.vue';
|
||||
import CategoryTreeSelect from './CategoryTreeSelect.vue';
|
||||
import SaveFavoriteDialog from './SaveFavoriteDialog.vue';
|
||||
import TagManager from './TagManager.vue';
|
||||
import {
|
||||
Search,
|
||||
DotsVertical,
|
||||
Upload,
|
||||
Download,
|
||||
Trash,
|
||||
Plus
|
||||
Plus,
|
||||
Tags,
|
||||
Folder
|
||||
} from '@vicons/tabler';
|
||||
import type { FavoritePrompt, FavoriteCategory } from '@prompt-optimizer/core';
|
||||
import type { AppServices } from '../types/services';
|
||||
@@ -303,6 +314,11 @@ const services = inject<Ref<AppServices | null> | null>('services', null);
|
||||
|
||||
const message = useToast();
|
||||
|
||||
// 初始化默认分类(仅在首次使用时创建)
|
||||
const { ensureDefaultCategories } = services?.value?.favoriteManager
|
||||
? useFavoriteInitializer(services.value.favoriteManager)
|
||||
: { ensureDefaultCategories: async () => {} };
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false);
|
||||
const favorites = ref<FavoritePrompt[]>([]);
|
||||
@@ -327,6 +343,7 @@ const createState = reactive({
|
||||
});
|
||||
const previewFavorite = ref<FavoritePrompt | null>(null);
|
||||
const categoryManagerVisible = ref(false);
|
||||
const tagManagerVisible = ref(false);
|
||||
|
||||
// 响应式的视口宽度
|
||||
const viewportWidth = ref(typeof window !== 'undefined' ? window.innerWidth : 1280);
|
||||
@@ -481,6 +498,19 @@ const pageSize = computed(() => {
|
||||
});
|
||||
|
||||
const actionMenuOptions = computed(() => [
|
||||
{
|
||||
label: t('favorites.manager.actions.manageTags'),
|
||||
key: 'manageTags',
|
||||
icon: () => h(NIcon, null, { default: () => h(Tags) })
|
||||
},
|
||||
{
|
||||
label: t('favorites.manager.actions.manageCategories'),
|
||||
key: 'manageCategories',
|
||||
icon: () => h(NIcon, null, { default: () => h(Folder) })
|
||||
},
|
||||
{
|
||||
type: 'divider'
|
||||
},
|
||||
{
|
||||
label: t('favorites.manager.actions.export'),
|
||||
key: 'export',
|
||||
@@ -759,6 +789,12 @@ const handleUseFavorite = (favorite: FavoritePrompt) => {
|
||||
|
||||
const handleActionMenuSelect = (key: string) => {
|
||||
switch (key) {
|
||||
case 'manageTags':
|
||||
tagManagerVisible.value = true;
|
||||
break;
|
||||
case 'manageCategories':
|
||||
categoryManagerVisible.value = true;
|
||||
break;
|
||||
case 'export':
|
||||
handleExportFavorites();
|
||||
break;
|
||||
@@ -884,29 +920,32 @@ watch(() => editState.visible, (visible) => {
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 窗口大小变化处理
|
||||
const handleResize = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
viewportWidth.value = window.innerWidth;
|
||||
}
|
||||
};
|
||||
const debouncedResize = useDebounceFn(handleResize, 150);
|
||||
|
||||
// 生命周期
|
||||
onMounted(async () => {
|
||||
if (services?.value?.favoriteManager) {
|
||||
// 确保默认分类存在(仅在首次使用时创建)
|
||||
await ensureDefaultCategories();
|
||||
loadFavorites();
|
||||
loadCategories();
|
||||
}
|
||||
|
||||
// 监听窗口大小变化
|
||||
const handleResize = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
viewportWidth.value = window.innerWidth;
|
||||
}
|
||||
};
|
||||
const debouncedResize = useDebounceFn(handleResize, 150);
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('resize', debouncedResize);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('resize', debouncedResize);
|
||||
}
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('resize', debouncedResize);
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
|
||||
@@ -84,11 +84,31 @@
|
||||
|
||||
<!-- 标签(跨越两列) -->
|
||||
<n-form-item :label="t('favorites.dialog.tagsLabel')">
|
||||
<n-dynamic-tags
|
||||
v-model:value="formData.tags"
|
||||
:max="10"
|
||||
:placeholder="t('favorites.dialog.tagsPlaceholder')"
|
||||
/>
|
||||
<div style="width: 100%;">
|
||||
<!-- 已选标签显示 -->
|
||||
<n-space v-if="formData.tags.length > 0" :size="[8, 8]" style="margin-bottom: 8px;">
|
||||
<n-tag
|
||||
v-for="(tag, index) in formData.tags"
|
||||
:key="tag"
|
||||
closable
|
||||
@close="handleRemoveTag(index)"
|
||||
type="info"
|
||||
>
|
||||
{{ tag }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
|
||||
<!-- 标签输入自动完成 -->
|
||||
<n-auto-complete
|
||||
v-model:value="tagInputValue"
|
||||
:options="tagSuggestions"
|
||||
:placeholder="t('favorites.dialog.tagsPlaceholder')"
|
||||
:get-show="() => tagInputValue.length > 0 || tagSuggestions.length > 0"
|
||||
clearable
|
||||
@select="handleSelectTag"
|
||||
@keydown.enter="handleAddTag"
|
||||
/>
|
||||
</div>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
@@ -131,7 +151,8 @@ import {
|
||||
NFormItem,
|
||||
NInput,
|
||||
NSelect,
|
||||
NDynamicTags,
|
||||
NAutoComplete,
|
||||
NTag,
|
||||
NButton,
|
||||
NSpace,
|
||||
NScrollbar,
|
||||
@@ -141,11 +162,13 @@ import {
|
||||
} from 'naive-ui';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useToast } from '../composables/useToast';
|
||||
import { useTagSuggestions } from '../composables/useTagSuggestions';
|
||||
import OutputDisplayCore from './OutputDisplayCore.vue';
|
||||
import CategoryTreeSelect from './CategoryTreeSelect.vue';
|
||||
import type { AppServices } from '../types/services';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { filterTags, loadTags } = useTagSuggestions();
|
||||
|
||||
interface Props {
|
||||
/** 是否显示对话框 */
|
||||
@@ -183,6 +206,16 @@ const message = useToast();
|
||||
|
||||
const saving = ref(false);
|
||||
|
||||
// 标签输入和建议
|
||||
const tagInputValue = ref('');
|
||||
const tagSuggestions = computed(() => {
|
||||
const suggestions = filterTags(tagInputValue.value, formData.tags);
|
||||
return suggestions.map(s => ({
|
||||
label: s.label,
|
||||
value: s.value
|
||||
}));
|
||||
});
|
||||
|
||||
// 对话框标题
|
||||
const dialogTitle = computed(() => {
|
||||
if (props.mode === 'create') return t('favorites.dialog.createTitle');
|
||||
@@ -234,6 +267,27 @@ const handleFunctionModeChange = (mode: 'basic' | 'context' | 'image') => {
|
||||
}
|
||||
};
|
||||
|
||||
// 标签管理函数
|
||||
const handleRemoveTag = (index: number) => {
|
||||
formData.tags.splice(index, 1);
|
||||
};
|
||||
|
||||
const handleSelectTag = (value: string) => {
|
||||
if (value && !formData.tags.includes(value) && formData.tags.length < 10) {
|
||||
formData.tags.push(value);
|
||||
tagInputValue.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddTag = (e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
const trimmedValue = tagInputValue.value.trim();
|
||||
if (trimmedValue && !formData.tags.includes(trimmedValue) && formData.tags.length < 10) {
|
||||
formData.tags.push(trimmedValue);
|
||||
tagInputValue.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
emit('update:show', false);
|
||||
@@ -260,6 +314,21 @@ const handleSave = async () => {
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
// 【优化】保存收藏前,确保所有标签都存在于独立标签库中
|
||||
// 这样可以保证:1. 标签统一管理 2. 导出时完整 3. 标签管理器能看到所有标签
|
||||
for (const tag of formData.tags) {
|
||||
try {
|
||||
await servicesValue.favoriteManager.addTag(tag);
|
||||
} catch (error: any) {
|
||||
// 只忽略"标签已存在"错误,其他错误需要抛出
|
||||
if (error?.code !== 'TAG_ALREADY_EXISTS') {
|
||||
console.error('添加标签到独立库失败:', error);
|
||||
throw error;
|
||||
}
|
||||
// 标签已存在,这是正常情况,继续处理
|
||||
}
|
||||
}
|
||||
|
||||
if (props.mode === 'edit' && props.favorite) {
|
||||
// 编辑模式:更新现有收藏
|
||||
await servicesValue.favoriteManager.updateFavorite(props.favorite.id, {
|
||||
@@ -308,8 +377,11 @@ const handleSave = async () => {
|
||||
};
|
||||
|
||||
// 监听对话框显示,初始化表单
|
||||
watch(() => props.show, (newShow) => {
|
||||
watch(() => props.show, async (newShow) => {
|
||||
if (newShow) {
|
||||
// 加载标签建议
|
||||
await loadTags();
|
||||
|
||||
if (props.mode === 'create') {
|
||||
// 新建模式: 重置为空表单
|
||||
formData.title = '';
|
||||
|
||||
@@ -0,0 +1,425 @@
|
||||
<template>
|
||||
<n-modal
|
||||
:show="show"
|
||||
preset="card"
|
||||
:title="t('favorites.manager.tagManager.title')"
|
||||
:style="{ width: 'min(90vw, 900px)', height: 'min(80vh, 700px)' }"
|
||||
:mask-closable="true"
|
||||
@update:show="$emit('update:show', $event)"
|
||||
>
|
||||
<n-space vertical :size="16" style="height: 100%;">
|
||||
<!-- 搜索栏和统计 -->
|
||||
<n-space justify="space-between" align="center">
|
||||
<n-input
|
||||
v-model:value="searchQuery"
|
||||
:placeholder="t('favorites.manager.tagManager.searchPlaceholder')"
|
||||
clearable
|
||||
style="flex: 1; max-width: 300px;"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon><Search /></n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
<n-space align="center" :size="12">
|
||||
<n-text depth="3">
|
||||
{{ t('favorites.manager.tagManager.totalTags', { count: filteredTags.length }) }}
|
||||
</n-text>
|
||||
<n-button type="primary" size="small" @click="handleAdd">
|
||||
{{ t('favorites.manager.tagManager.add') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-space>
|
||||
|
||||
<!-- 标签表格 -->
|
||||
<n-data-table
|
||||
:columns="columns"
|
||||
:data="filteredTags"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:bordered="false"
|
||||
:max-height="450"
|
||||
striped
|
||||
/>
|
||||
</n-space>
|
||||
|
||||
<!-- 新增标签对话框 -->
|
||||
<n-modal
|
||||
v-model:show="showAddDialog"
|
||||
preset="dialog"
|
||||
:title="t('favorites.manager.tagManager.addDialog.title')"
|
||||
:positive-text="t('favorites.manager.tagManager.addDialog.confirm')"
|
||||
:negative-text="t('favorites.manager.tagManager.addDialog.cancel')"
|
||||
@positive-click="handleAddConfirm"
|
||||
>
|
||||
<n-space vertical :size="12">
|
||||
<n-input
|
||||
v-model:value="newTagName"
|
||||
:placeholder="t('favorites.manager.tagManager.addDialog.tagNamePlaceholder')"
|
||||
@keydown.enter="handleAddConfirm"
|
||||
/>
|
||||
</n-space>
|
||||
</n-modal>
|
||||
|
||||
<!-- 重命名对话框 -->
|
||||
<n-modal
|
||||
v-model:show="showRenameDialog"
|
||||
preset="dialog"
|
||||
:title="t('favorites.manager.tagManager.renameDialog.title')"
|
||||
:positive-text="t('favorites.manager.tagManager.renameDialog.confirm')"
|
||||
:negative-text="t('favorites.manager.tagManager.renameDialog.cancel')"
|
||||
@positive-click="handleRenameConfirm"
|
||||
>
|
||||
<n-space vertical :size="12">
|
||||
<n-text>{{ t('favorites.manager.tagManager.renameDialog.currentName', { name: currentTag?.name }) }}</n-text>
|
||||
<n-input
|
||||
v-model:value="newTagName"
|
||||
:placeholder="t('favorites.manager.tagManager.renameDialog.newNamePlaceholder')"
|
||||
@keydown.enter="handleRenameConfirm"
|
||||
/>
|
||||
</n-space>
|
||||
</n-modal>
|
||||
|
||||
<!-- 合并对话框 -->
|
||||
<n-modal
|
||||
v-model:show="showMergeDialog"
|
||||
preset="dialog"
|
||||
:title="t('favorites.manager.tagManager.mergeDialog.title')"
|
||||
:positive-text="t('favorites.manager.tagManager.mergeDialog.confirm')"
|
||||
:negative-text="t('favorites.manager.tagManager.mergeDialog.cancel')"
|
||||
@positive-click="handleMergeConfirm"
|
||||
>
|
||||
<n-space vertical :size="12">
|
||||
<n-text>{{ t('favorites.manager.tagManager.mergeDialog.sourceTag', { name: currentTag?.name }) }}</n-text>
|
||||
<n-select
|
||||
v-model:value="mergeTargetTag"
|
||||
:options="mergeTargetOptions"
|
||||
:placeholder="t('favorites.manager.tagManager.mergeDialog.targetPlaceholder')"
|
||||
filterable
|
||||
/>
|
||||
</n-space>
|
||||
</n-modal>
|
||||
|
||||
<template #action>
|
||||
<n-button @click="$emit('update:show', false)">
|
||||
{{ t('favorites.manager.tagManager.close') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, inject, watch, h, type Ref } from 'vue';
|
||||
import {
|
||||
NModal,
|
||||
NSpace,
|
||||
NInput,
|
||||
NButton,
|
||||
NDataTable,
|
||||
NTag,
|
||||
NIcon,
|
||||
NText,
|
||||
NPopconfirm,
|
||||
NSelect,
|
||||
NTooltip,
|
||||
type DataTableColumns
|
||||
} from 'naive-ui';
|
||||
import { Search, Edit, Trash, GitMerge } from '@vicons/tabler';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useToast } from '../composables/useToast';
|
||||
import type { AppServices } from '../types/services';
|
||||
import type { TagStatistics } from '@prompt-optimizer/core';
|
||||
import { TagTypeConverter } from '@prompt-optimizer/core';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:show': [value: boolean];
|
||||
'updated': [];
|
||||
}>();
|
||||
|
||||
const services = inject<Ref<AppServices | null>>('services');
|
||||
const message = useToast();
|
||||
|
||||
const loading = ref(false);
|
||||
const allTags = ref<TagStatistics[]>([]);
|
||||
const searchQuery = ref('');
|
||||
|
||||
// 新增相关
|
||||
const showAddDialog = ref(false);
|
||||
|
||||
// 重命名相关
|
||||
const showRenameDialog = ref(false);
|
||||
const currentTag = ref<TagStatistics | null>(null);
|
||||
const newTagName = ref('');
|
||||
|
||||
// 合并相关
|
||||
const showMergeDialog = ref(false);
|
||||
const mergeTargetTag = ref<string>('');
|
||||
|
||||
// 分页配置
|
||||
const pagination = {
|
||||
pageSize: 10,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20, 50, 100]
|
||||
};
|
||||
|
||||
// 过滤后的标签
|
||||
const filteredTags = computed(() => {
|
||||
if (!searchQuery.value) {
|
||||
return allTags.value;
|
||||
}
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return allTags.value.filter(tag => tag.name.toLowerCase().includes(query));
|
||||
});
|
||||
|
||||
// 合并目标选项(排除当前标签)
|
||||
const mergeTargetOptions = computed(() => {
|
||||
return allTags.value
|
||||
.filter(tag => tag.name !== currentTag.value?.name)
|
||||
.map(tag => ({
|
||||
label: `${tag.name} (${tag.count})`,
|
||||
value: tag.name
|
||||
}));
|
||||
});
|
||||
|
||||
// 表格列定义
|
||||
const columns = computed<DataTableColumns<TagStatistics>>(() => [
|
||||
{
|
||||
title: t('favorites.manager.tagManager.tagName'),
|
||||
key: 'name',
|
||||
width: 250,
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
},
|
||||
render: (row) => {
|
||||
return h(NTag, { type: 'info' }, { default: () => row.name });
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('favorites.manager.tagManager.useCount'),
|
||||
key: 'count',
|
||||
width: 150,
|
||||
align: 'center' as const,
|
||||
sorter: (a, b) => a.count - b.count
|
||||
},
|
||||
{
|
||||
title: t('favorites.manager.tagManager.actions'),
|
||||
key: 'actions',
|
||||
width: 250,
|
||||
align: 'center' as const,
|
||||
render: (row) => {
|
||||
return h(NSpace, { size: 4, justify: 'center' }, {
|
||||
default: () => [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
quaternary: true,
|
||||
onClick: () => handleRename(row)
|
||||
},
|
||||
{
|
||||
icon: () => h(NIcon, null, { default: () => h(Edit) }),
|
||||
default: () => t('favorites.manager.tagManager.rename')
|
||||
}
|
||||
),
|
||||
h(
|
||||
NTooltip,
|
||||
{},
|
||||
{
|
||||
trigger: () => h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
quaternary: true,
|
||||
onClick: () => handleMerge(row)
|
||||
},
|
||||
{
|
||||
icon: () => h(NIcon, null, { default: () => h(GitMerge) }),
|
||||
default: () => t('favorites.manager.tagManager.merge')
|
||||
}
|
||||
),
|
||||
default: () => t('favorites.manager.tagManager.mergeTooltip')
|
||||
}
|
||||
),
|
||||
h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => handleDelete(row),
|
||||
positiveText: t('favorites.manager.tagManager.renameDialog.confirm'),
|
||||
negativeText: t('favorites.manager.tagManager.renameDialog.cancel')
|
||||
},
|
||||
{
|
||||
trigger: () => h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
quaternary: true,
|
||||
type: 'error'
|
||||
},
|
||||
{
|
||||
icon: () => h(NIcon, null, { default: () => h(Trash) }),
|
||||
default: () => t('favorites.manager.tagManager.delete')
|
||||
}
|
||||
),
|
||||
default: () => t('favorites.manager.tagManager.deleteConfirm', { name: row.name, count: row.count })
|
||||
}
|
||||
)
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
// 加载标签数据
|
||||
const loadTags = async () => {
|
||||
const servicesValue = services?.value;
|
||||
if (!servicesValue?.favoriteManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const tags = await servicesValue.favoriteManager.getAllTags();
|
||||
// 使用统一的类型转换器转换数据格式
|
||||
allTags.value = TagTypeConverter.toTagStatistics(tags);
|
||||
} catch (error: any) {
|
||||
message.error(t('favorites.manager.tagManager.messages.loadFailed') + `: ${error?.message || '未知错误'}`);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 新增标签
|
||||
const handleAdd = () => {
|
||||
newTagName.value = '';
|
||||
showAddDialog.value = true;
|
||||
};
|
||||
|
||||
const handleAddConfirm = async () => {
|
||||
const servicesValue = services?.value;
|
||||
if (!servicesValue?.favoriteManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
const trimmedName = newTagName.value.trim();
|
||||
if (!trimmedName) {
|
||||
message.warning(t('favorites.manager.tagManager.addDialog.emptyWarning'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查标签是否已存在
|
||||
if (allTags.value.some(tag => tag.name === trimmedName)) {
|
||||
message.warning(t('favorites.manager.tagManager.addDialog.existWarning'));
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 调用 addTag API 持久化标签
|
||||
await servicesValue.favoriteManager.addTag(trimmedName);
|
||||
|
||||
// 重新加载标签列表
|
||||
await loadTags();
|
||||
|
||||
message.success(t('favorites.manager.tagManager.messages.addSuccess'));
|
||||
showAddDialog.value = false;
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
message.error(t('favorites.manager.tagManager.messages.addFailed') + `: ${error?.message || '未知错误'}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 重命名标签
|
||||
const handleRename = (tag: TagStatistics) => {
|
||||
currentTag.value = tag;
|
||||
newTagName.value = tag.name;
|
||||
showRenameDialog.value = true;
|
||||
};
|
||||
|
||||
const handleRenameConfirm = async () => {
|
||||
const servicesValue = services?.value;
|
||||
if (!servicesValue?.favoriteManager || !currentTag.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const trimmedName = newTagName.value.trim();
|
||||
if (!trimmedName) {
|
||||
message.warning(t('favorites.manager.tagManager.renameDialog.emptyWarning'));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (trimmedName === currentTag.value.name) {
|
||||
showRenameDialog.value = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
await servicesValue.favoriteManager.renameTag(currentTag.value.name, trimmedName);
|
||||
message.success(t('favorites.manager.tagManager.messages.renameSuccess'));
|
||||
await loadTags();
|
||||
emit('updated');
|
||||
showRenameDialog.value = false;
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
message.error(t('favorites.manager.tagManager.messages.renameFailed') + `: ${error?.message || '未知错误'}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 合并标签
|
||||
const handleMerge = (tag: TagStatistics) => {
|
||||
currentTag.value = tag;
|
||||
mergeTargetTag.value = '';
|
||||
showMergeDialog.value = true;
|
||||
};
|
||||
|
||||
const handleMergeConfirm = async () => {
|
||||
const servicesValue = services?.value;
|
||||
if (!servicesValue?.favoriteManager || !currentTag.value || !mergeTargetTag.value) {
|
||||
message.warning(t('favorites.manager.tagManager.mergeDialog.selectTargetWarning'));
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await servicesValue.favoriteManager.mergeTags([currentTag.value.name], mergeTargetTag.value);
|
||||
message.success(t('favorites.manager.tagManager.messages.mergeSuccess'));
|
||||
await loadTags();
|
||||
emit('updated');
|
||||
showMergeDialog.value = false;
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
message.error(t('favorites.manager.tagManager.messages.mergeFailed') + `: ${error?.message || '未知错误'}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 删除标签
|
||||
const handleDelete = async (tag: TagStatistics) => {
|
||||
const servicesValue = services?.value;
|
||||
if (!servicesValue?.favoriteManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await servicesValue.favoriteManager.deleteTag(tag.name);
|
||||
message.success(t('favorites.manager.tagManager.messages.deleteSuccess'));
|
||||
await loadTags();
|
||||
emit('updated');
|
||||
} catch (error: any) {
|
||||
message.error(t('favorites.manager.tagManager.messages.deleteFailed') + `: ${error?.message || '未知错误'}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听对话框显示状态,打开时加载数据
|
||||
watch(() => props.show, (newShow) => {
|
||||
if (newShow) {
|
||||
loadTags();
|
||||
}
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { FavoriteManager } from '@prompt-optimizer/core';
|
||||
|
||||
/**
|
||||
* 收藏功能初始化器
|
||||
* 负责创建国际化的默认分类
|
||||
*/
|
||||
export function useFavoriteInitializer(manager: FavoriteManager) {
|
||||
const { t } = useI18n();
|
||||
|
||||
/**
|
||||
* 确保默认分类存在(仅首次使用时创建)
|
||||
*/
|
||||
const ensureDefaultCategories = async () => {
|
||||
const defaultCategories = [
|
||||
{
|
||||
name: t('favorites.categories.default.uncategorized'),
|
||||
description: t('favorites.categories.default.uncategorizedDesc'),
|
||||
color: '#6B7280'
|
||||
},
|
||||
{
|
||||
name: t('favorites.categories.default.creativeWriting'),
|
||||
description: t('favorites.categories.default.creativeWritingDesc'),
|
||||
color: '#8B5CF6'
|
||||
},
|
||||
{
|
||||
name: t('favorites.categories.default.programming'),
|
||||
description: t('favorites.categories.default.programmingDesc'),
|
||||
color: '#F59E0B'
|
||||
},
|
||||
{
|
||||
name: t('favorites.categories.default.businessAnalysis'),
|
||||
description: t('favorites.categories.default.businessAnalysisDesc'),
|
||||
color: '#EF4444'
|
||||
},
|
||||
{
|
||||
name: t('favorites.categories.default.learning'),
|
||||
description: t('favorites.categories.default.learningDesc'),
|
||||
color: '#10B981'
|
||||
},
|
||||
{
|
||||
name: t('favorites.categories.default.dailyAssistant'),
|
||||
description: t('favorites.categories.default.dailyAssistantDesc'),
|
||||
color: '#3B82F6'
|
||||
}
|
||||
];
|
||||
|
||||
await manager.ensureDefaultCategories(defaultCategories);
|
||||
};
|
||||
|
||||
return {
|
||||
ensureDefaultCategories
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { ref, computed, inject, type Ref } from 'vue';
|
||||
import type { AppServices } from '../types/services';
|
||||
import { TagTypeConverter } from '@prompt-optimizer/core';
|
||||
|
||||
export interface TagSuggestion {
|
||||
label: string;
|
||||
value: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签建议 Composable
|
||||
* 提供标签自动完成功能,基于现有收藏中的标签使用情况
|
||||
*/
|
||||
export function useTagSuggestions() {
|
||||
const services = inject<Ref<AppServices | null>>('services');
|
||||
const allTags = ref<TagSuggestion[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
/**
|
||||
* 加载所有标签统计数据
|
||||
*/
|
||||
const loadTags = async () => {
|
||||
if (!services?.value?.favoriteManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const tagStats = await services.value.favoriteManager.getAllTags();
|
||||
// 使用统一的类型转换器转换为自动完成选项格式
|
||||
allTags.value = TagTypeConverter.toAutoCompleteOptions(tagStats);
|
||||
} catch (error) {
|
||||
console.error('Failed to load tags:', error);
|
||||
allTags.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据输入查询过滤标签建议
|
||||
* @param query 查询字符串
|
||||
* @param excludeTags 需要排除的标签(已选中的标签)
|
||||
* @returns 过滤后的标签建议列表
|
||||
*/
|
||||
const filterTags = (query: string, excludeTags: string[] = []): TagSuggestion[] => {
|
||||
if (!query) {
|
||||
// 如果没有输入,返回所有未选中的标签,按使用次数排序
|
||||
return allTags.value
|
||||
.filter(tag => !excludeTags.includes(tag.value))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
}
|
||||
|
||||
// 模糊搜索匹配
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return allTags.value
|
||||
.filter(tag => {
|
||||
// 排除已选中的标签
|
||||
if (excludeTags.includes(tag.value)) {
|
||||
return false;
|
||||
}
|
||||
// 包含查询字符串
|
||||
return tag.value.toLowerCase().includes(lowerQuery);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// 优先匹配前缀
|
||||
const aStartsWith = a.value.toLowerCase().startsWith(lowerQuery);
|
||||
const bStartsWith = b.value.toLowerCase().startsWith(lowerQuery);
|
||||
if (aStartsWith && !bStartsWith) return -1;
|
||||
if (!aStartsWith && bStartsWith) return 1;
|
||||
// 其次按使用次数排序
|
||||
return b.count - a.count;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取热门标签(使用次数最多的前N个)
|
||||
* @param limit 返回数量限制
|
||||
* @param excludeTags 需要排除的标签
|
||||
*/
|
||||
const getPopularTags = computed(() => {
|
||||
return (limit = 10, excludeTags: string[] = []): TagSuggestion[] => {
|
||||
return allTags.value
|
||||
.filter(tag => !excludeTags.includes(tag.value))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, limit);
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取最近使用的标签(暂时与热门标签相同,未来可以基于时间戳优化)
|
||||
* @param limit 返回数量限制
|
||||
* @param excludeTags 需要排除的标签
|
||||
*/
|
||||
const getRecentTags = computed(() => {
|
||||
return (limit = 10, excludeTags: string[] = []): TagSuggestion[] => {
|
||||
// TODO: 未来可以基于收藏的更新时间来优化这个逻辑
|
||||
return getPopularTags.value(limit, excludeTags);
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
allTags,
|
||||
loading,
|
||||
loadTags,
|
||||
filterTags,
|
||||
getPopularTags,
|
||||
getRecentTags
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,20 @@
|
||||
const favoriteCategoriesMessages = {
|
||||
default: {
|
||||
uncategorized: 'Uncategorized',
|
||||
uncategorizedDesc: 'Default category',
|
||||
creativeWriting: 'Creative Writing',
|
||||
creativeWritingDesc: 'Creative writing prompts',
|
||||
programming: 'Programming',
|
||||
programmingDesc: 'Programming and development prompts',
|
||||
businessAnalysis: 'Business Analysis',
|
||||
businessAnalysisDesc: 'Business analysis prompts',
|
||||
learning: 'Learning & Research',
|
||||
learningDesc: 'Learning and research prompts',
|
||||
dailyAssistant: 'Daily Assistant',
|
||||
dailyAssistantDesc: 'Daily life assistant prompts'
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
common: {
|
||||
loading: 'Loading...',
|
||||
@@ -103,6 +120,7 @@ export default {
|
||||
},
|
||||
favorites: {
|
||||
title: 'My Favorites',
|
||||
categories: favoriteCategoriesMessages,
|
||||
manager: {
|
||||
title: 'Favorites Manager',
|
||||
searchPlaceholder: 'Search favorites...',
|
||||
@@ -138,6 +156,8 @@ export default {
|
||||
title: 'Category Manager'
|
||||
},
|
||||
actions: {
|
||||
manageTags: 'Manage Tags',
|
||||
manageCategories: 'Manage Categories',
|
||||
export: 'Export Favorites',
|
||||
clear: 'Clear Favorites',
|
||||
exportSuccess: 'Export successful',
|
||||
@@ -180,7 +200,77 @@ export default {
|
||||
edit: 'Edit',
|
||||
delete: 'Delete',
|
||||
cancel: 'Cancel',
|
||||
deleteConfirm: 'Delete "{title}"?'
|
||||
deleteConfirm: 'Delete "{title}"?',
|
||||
functionMode: {
|
||||
basic: 'Basic',
|
||||
context: 'Context',
|
||||
image: 'Image'
|
||||
},
|
||||
optimizationMode: {
|
||||
system: 'System',
|
||||
user: 'User'
|
||||
},
|
||||
imageSubMode: {
|
||||
text2image: 'Text-to-Image',
|
||||
image2image: 'Image-to-Image'
|
||||
}
|
||||
},
|
||||
categories: favoriteCategoriesMessages,
|
||||
tagManager: {
|
||||
title: 'Tag Manager',
|
||||
searchPlaceholder: 'Search tags...',
|
||||
totalTags: 'Total {count} tags',
|
||||
add: 'Add Tag',
|
||||
tagName: 'Tag Name',
|
||||
useCount: 'Use Count',
|
||||
actions: 'Actions',
|
||||
rename: 'Rename',
|
||||
merge: 'Merge',
|
||||
mergeTooltip: 'Merge current tag into target tag',
|
||||
delete: 'Delete',
|
||||
close: 'Close',
|
||||
addDialog: {
|
||||
title: 'Add Tag',
|
||||
tagName: 'Tag Name',
|
||||
tagNamePlaceholder: 'Enter tag name',
|
||||
emptyWarning: 'Tag name cannot be empty',
|
||||
existWarning: 'Tag already exists',
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel'
|
||||
},
|
||||
renameDialog: {
|
||||
title: 'Rename Tag',
|
||||
oldName: 'Original Name',
|
||||
newName: 'New Name',
|
||||
newNamePlaceholder: 'Enter new tag name',
|
||||
currentName: 'Current tag name: {name}',
|
||||
emptyWarning: 'Tag name cannot be empty',
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel'
|
||||
},
|
||||
mergeDialog: {
|
||||
title: 'Merge Tags',
|
||||
sourceName: 'Source Tag',
|
||||
targetName: 'Merge Into',
|
||||
targetPlaceholder: 'Select target tag',
|
||||
sourceTag: 'Source tag: {name}',
|
||||
selectTargetWarning: 'Please select target tag',
|
||||
confirm: 'Confirm Merge',
|
||||
cancel: 'Cancel',
|
||||
warning: 'After merging, "{source}" tag will be deleted, and all favorites using this tag will use "{target}" tag instead'
|
||||
},
|
||||
deleteConfirm: 'Are you sure you want to delete tag "{name}"? This will remove the tag from {count} favorites.',
|
||||
messages: {
|
||||
loadFailed: 'Failed to load tag list',
|
||||
addSuccess: 'Tag added successfully',
|
||||
addFailed: 'Failed to add tag',
|
||||
renameSuccess: 'Tag renamed successfully',
|
||||
renameFailed: 'Failed to rename tag',
|
||||
mergeSuccess: 'Tags merged successfully',
|
||||
mergeFailed: 'Failed to merge tags',
|
||||
deleteSuccess: 'Tag deleted successfully',
|
||||
deleteFailed: 'Failed to delete tag'
|
||||
}
|
||||
}
|
||||
},
|
||||
dialog: {
|
||||
|
||||
@@ -1,4 +1,21 @@
|
||||
export default {
|
||||
const favoriteCategoriesMessages = {
|
||||
default: {
|
||||
uncategorized: '未分类',
|
||||
uncategorizedDesc: '默认分类',
|
||||
creativeWriting: '创意写作',
|
||||
creativeWritingDesc: '创意写作相关的提示词',
|
||||
programming: '编程开发',
|
||||
programmingDesc: '编程开发相关的提示词',
|
||||
businessAnalysis: '商业分析',
|
||||
businessAnalysisDesc: '商业分析相关的提示词',
|
||||
learning: '学习研究',
|
||||
learningDesc: '学习研究相关的提示词',
|
||||
dailyAssistant: '日常助手',
|
||||
dailyAssistantDesc: '日常生活助手类提示词'
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
common: {
|
||||
loading: '加载中...',
|
||||
save: '保存',
|
||||
@@ -103,6 +120,7 @@
|
||||
},
|
||||
favorites: {
|
||||
title: '我的收藏',
|
||||
categories: favoriteCategoriesMessages,
|
||||
manager: {
|
||||
title: '收藏管理',
|
||||
searchPlaceholder: '搜索收藏...',
|
||||
@@ -138,6 +156,8 @@
|
||||
title: '分类管理'
|
||||
},
|
||||
actions: {
|
||||
manageTags: '管理标签',
|
||||
manageCategories: '管理分类',
|
||||
export: '导出收藏',
|
||||
clear: '清空收藏',
|
||||
exportSuccess: '导出成功',
|
||||
@@ -180,7 +200,77 @@
|
||||
edit: '编辑',
|
||||
delete: '删除',
|
||||
cancel: '取消',
|
||||
deleteConfirm: '确定删除"{title}"吗?'
|
||||
deleteConfirm: '确定删除"{title}"吗?',
|
||||
functionMode: {
|
||||
basic: '基础',
|
||||
context: '上下文',
|
||||
image: '图像'
|
||||
},
|
||||
optimizationMode: {
|
||||
system: '系统',
|
||||
user: '用户'
|
||||
},
|
||||
imageSubMode: {
|
||||
text2image: '文生图',
|
||||
image2image: '图生图'
|
||||
}
|
||||
},
|
||||
categories: favoriteCategoriesMessages,
|
||||
tagManager: {
|
||||
title: '标签管理',
|
||||
searchPlaceholder: '搜索标签...',
|
||||
totalTags: '共 {count} 个标签',
|
||||
add: '新增标签',
|
||||
tagName: '标签名',
|
||||
useCount: '使用次数',
|
||||
actions: '操作',
|
||||
rename: '重命名',
|
||||
merge: '合并',
|
||||
mergeTooltip: '将当前标签合并到目标标签中',
|
||||
delete: '删除',
|
||||
close: '关闭',
|
||||
addDialog: {
|
||||
title: '新增标签',
|
||||
tagName: '标签名称',
|
||||
tagNamePlaceholder: '输入标签名称',
|
||||
emptyWarning: '标签名不能为空',
|
||||
existWarning: '标签已存在',
|
||||
confirm: '确认',
|
||||
cancel: '取消'
|
||||
},
|
||||
renameDialog: {
|
||||
title: '重命名标签',
|
||||
oldName: '原标签名',
|
||||
newName: '新标签名',
|
||||
newNamePlaceholder: '输入新的标签名',
|
||||
currentName: '当前标签名:{name}',
|
||||
emptyWarning: '标签名不能为空',
|
||||
confirm: '确认',
|
||||
cancel: '取消'
|
||||
},
|
||||
mergeDialog: {
|
||||
title: '合并标签',
|
||||
sourceName: '源标签',
|
||||
targetName: '合并到',
|
||||
targetPlaceholder: '选择目标标签',
|
||||
sourceTag: '源标签:{name}',
|
||||
selectTargetWarning: '请选择目标标签',
|
||||
confirm: '确认合并',
|
||||
cancel: '取消',
|
||||
warning: '合并后,"{source}" 标签将被删除,所有使用该标签的收藏将改用 "{target}" 标签'
|
||||
},
|
||||
deleteConfirm: '确定删除标签 "{name}"?这将从 {count} 个收藏中移除该标签。',
|
||||
messages: {
|
||||
loadFailed: '加载标签列表失败',
|
||||
addSuccess: '标签添加成功',
|
||||
addFailed: '标签添加失败',
|
||||
renameSuccess: '标签重命名成功',
|
||||
renameFailed: '标签重命名失败',
|
||||
mergeSuccess: '标签合并成功',
|
||||
mergeFailed: '标签合并失败',
|
||||
deleteSuccess: '标签删除成功',
|
||||
deleteFailed: '标签删除失败'
|
||||
}
|
||||
}
|
||||
},
|
||||
dialog: {
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
const favoriteCategoriesMessages = {
|
||||
default: {
|
||||
uncategorized: '未分類',
|
||||
uncategorizedDesc: '預設分類',
|
||||
creativeWriting: '創意寫作',
|
||||
creativeWritingDesc: '創意寫作相關的提示詞',
|
||||
programming: '程式開發',
|
||||
programmingDesc: '程式開發相關的提示詞',
|
||||
businessAnalysis: '商業分析',
|
||||
businessAnalysisDesc: '商業分析相關的提示詞',
|
||||
learning: '學習研究',
|
||||
learningDesc: '學習研究相關的提示詞',
|
||||
dailyAssistant: '日常助手',
|
||||
dailyAssistantDesc: '日常生活助手類提示詞'
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
common: {
|
||||
loading: '載入中...',
|
||||
@@ -103,6 +120,7 @@ export default {
|
||||
},
|
||||
favorites: {
|
||||
title: '我的收藏',
|
||||
categories: favoriteCategoriesMessages,
|
||||
manager: {
|
||||
title: '收藏管理',
|
||||
searchPlaceholder: '搜尋收藏...',
|
||||
@@ -138,6 +156,8 @@ export default {
|
||||
title: '分類管理'
|
||||
},
|
||||
actions: {
|
||||
manageTags: '管理標籤',
|
||||
manageCategories: '管理分類',
|
||||
export: '匯出收藏',
|
||||
clear: '清空收藏',
|
||||
exportSuccess: '匯出成功',
|
||||
@@ -180,7 +200,77 @@ export default {
|
||||
edit: '編輯',
|
||||
delete: '刪除',
|
||||
cancel: '取消',
|
||||
deleteConfirm: '確定刪除"{title}"嗎?'
|
||||
deleteConfirm: '確定刪除"{title}"嗎?',
|
||||
functionMode: {
|
||||
basic: '基礎',
|
||||
context: '情境',
|
||||
image: '圖像'
|
||||
},
|
||||
optimizationMode: {
|
||||
system: '系統',
|
||||
user: '使用者'
|
||||
},
|
||||
imageSubMode: {
|
||||
text2image: '文生圖',
|
||||
image2image: '圖生圖'
|
||||
}
|
||||
},
|
||||
categories: favoriteCategoriesMessages,
|
||||
tagManager: {
|
||||
title: '標籤管理',
|
||||
searchPlaceholder: '搜尋標籤...',
|
||||
totalTags: '共 {count} 個標籤',
|
||||
add: '新增標籤',
|
||||
tagName: '標籤名稱',
|
||||
useCount: '使用次數',
|
||||
actions: '操作',
|
||||
rename: '重新命名',
|
||||
merge: '合併',
|
||||
mergeTooltip: '將目前標籤合併到目標標籤中',
|
||||
delete: '刪除',
|
||||
close: '關閉',
|
||||
addDialog: {
|
||||
title: '新增標籤',
|
||||
tagName: '標籤名稱',
|
||||
tagNamePlaceholder: '輸入標籤名稱',
|
||||
emptyWarning: '標籤名稱不能為空',
|
||||
existWarning: '標籤已存在',
|
||||
confirm: '確認',
|
||||
cancel: '取消'
|
||||
},
|
||||
renameDialog: {
|
||||
title: '重新命名標籤',
|
||||
oldName: '原標籤名稱',
|
||||
newName: '新標籤名稱',
|
||||
newNamePlaceholder: '輸入新的標籤名稱',
|
||||
currentName: '目前標籤名稱:{name}',
|
||||
emptyWarning: '標籤名稱不能為空',
|
||||
confirm: '確認',
|
||||
cancel: '取消'
|
||||
},
|
||||
mergeDialog: {
|
||||
title: '合併標籤',
|
||||
sourceName: '源標籤',
|
||||
targetName: '合併到',
|
||||
targetPlaceholder: '選擇目標標籤',
|
||||
sourceTag: '源標籤:{name}',
|
||||
selectTargetWarning: '請選擇目標標籤',
|
||||
confirm: '確認合併',
|
||||
cancel: '取消',
|
||||
warning: '合併後,"{source}" 標籤將被刪除,所有使用該標籤的收藏將改用 "{target}" 標籤'
|
||||
},
|
||||
deleteConfirm: '確定刪除標籤 "{name}"?這將從 {count} 個收藏中移除該標籤。',
|
||||
messages: {
|
||||
loadFailed: '載入標籤清單失敗',
|
||||
addSuccess: '標籤新增成功',
|
||||
addFailed: '標籤新增失敗',
|
||||
renameSuccess: '標籤重新命名成功',
|
||||
renameFailed: '標籤重新命名失敗',
|
||||
mergeSuccess: '標籤合併成功',
|
||||
mergeFailed: '標籤合併失敗',
|
||||
deleteSuccess: '標籤刪除成功',
|
||||
deleteFailed: '標籤刪除失敗'
|
||||
}
|
||||
}
|
||||
},
|
||||
dialog: {
|
||||
|
||||
Reference in new issue
Block a user