From 75c17597eb6ec7c58101f0d4641aa677dbe22bf3 Mon Sep 17 00:00:00 2001 From: 0nikod Date: Thu, 14 May 2026 08:30:49 +0800 Subject: [PATCH] refactor frontend api client --- src/App.vue | 8 +-- src/api/client.ts | 114 ++++++++++++++++++++++++++++++ src/api/types.ts | 55 ++++++++++++++ src/components/CustomTagModal.vue | 13 +--- src/components/SettingsModal.vue | 36 +++------- src/components/TagManager.vue | 63 ++++------------- src/components/TagSearchModal.vue | 16 ++--- src/utils/categoryService.ts | 40 ++++------- src/utils/duckdbService.ts | 35 +++------ src/utils/metaService.ts | 40 +++++------ 10 files changed, 242 insertions(+), 178 deletions(-) create mode 100644 src/api/client.ts create mode 100644 src/api/types.ts diff --git a/src/App.vue b/src/App.vue index c8b13d1..91af699 100644 --- a/src/App.vue +++ b/src/App.vue @@ -7,6 +7,7 @@ import { CategoryService } from './utils/categoryService'; import { settings } from './utils/settings'; import { metaService } from './utils/metaService'; import { textToTags, applyAutoMeta } from './utils/promptParser'; +import { simplePromptApi } from './api/client'; const props = defineProps<{ initialPrompt: string; @@ -57,12 +58,7 @@ const saveChanges = async () => { let ratingTagNames: string[] = []; try { - const resp = await fetch('/simple-prompt/get-tags-details', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ names: tagNames, fast: true }) - }); - const categoriesMap = await resp.json(); + const categoriesMap = await simplePromptApi.getTagsDetails(tagNames, true); // Extract rating tag names (category 7) ratingTagNames = tagNames.filter(name => diff --git a/src/api/client.ts b/src/api/client.ts new file mode 100644 index 0000000..07bf925 --- /dev/null +++ b/src/api/client.ts @@ -0,0 +1,114 @@ +import type { + AddTagsPayload, + ApiStatusResponse, + CategoryRecord, + PresetsResponse, + PresetRecord, + TagRecord, + TagsListResponse, + ToggleLikeResponse, + UpdateCheckResponse, +} from './types'; + +async function requestJson(url: string, init?: RequestInit): Promise { + const response = await fetch(url, init); + const data = await response.json().catch(() => ({})); + + if (!response.ok) { + const message = typeof data?.error === 'string' ? data.error : response.statusText; + throw new Error(message || `HTTP error ${response.status}`); + } + + return data as T; +} + +function jsonRequest(method: string, body?: unknown): RequestInit { + return { + method, + headers: { 'Content-Type': 'application/json' }, + body: body === undefined ? undefined : JSON.stringify(body), + }; +} + +export const simplePromptApi = { + async searchTags(query: string, limit = 20, useAliases = false, categories: number[] = []): Promise { + const params = new URLSearchParams({ + q: query, + limit: limit.toString(), + use_aliases: useAliases.toString(), + categories: categories.join(','), + }); + + return requestJson(`/simple-prompt/search-tags?${params.toString()}`); + }, + + async getTagsDetails(names: string[], fast = false): Promise> { + if (names.length === 0) return {}; + + return requestJson>( + '/simple-prompt/get-tags-details', + jsonRequest('POST', { names, fast }), + ); + }, + + async listTags(source: string, limit: number, offset: number, query: string): Promise { + const params = new URLSearchParams({ + source, + limit: limit.toString(), + offset: offset.toString(), + q: query, + }); + + return requestJson(`/simple-prompt/tags/list?${params.toString()}`); + }, + + async deleteTag(name: string, source: string): Promise { + return requestJson('/simple-prompt/tags/delete', jsonRequest('DELETE', { name, source })); + }, + + async addTag(payload: AddTagsPayload): Promise { + return requestJson('/simple-prompt/add-custom-tag', jsonRequest('POST', payload)); + }, + + async addTags(tags: AddTagsPayload[]): Promise { + return requestJson('/simple-prompt/add-custom-tag', jsonRequest('POST', { tags })); + }, + + async toggleLikeTag(payload: { + name: string; + is_liked: boolean; + category?: number; + post_count?: number; + alias?: string[]; + }): Promise { + return requestJson('/simple-prompt/toggle-like-tag', jsonRequest('POST', payload)); + }, + + async listCategories(): Promise { + return requestJson('/simple-prompt/categories/list'); + }, + + async saveCategories(categories: CategoryRecord[]): Promise { + return requestJson('/simple-prompt/categories/save', jsonRequest('POST', { categories })); + }, + + async listPresets(): Promise { + return requestJson('/simple-prompt/presets/list'); + }, + + async savePresets(presets: PresetRecord[]): Promise { + return requestJson('/simple-prompt/presets/save', jsonRequest('POST', { presets })); + }, + + async checkUpdate(): Promise { + return requestJson('/simple-prompt/check-update'); + }, + + async updateTags(): Promise { + return requestJson('/simple-prompt/update-tags', jsonRequest('POST', {})); + }, + + async updateData(action: 'update_liked' | 'update_user'): Promise { + return requestJson('/simple-prompt/update-data', jsonRequest('POST', { action })); + }, +}; diff --git a/src/api/types.ts b/src/api/types.ts new file mode 100644 index 0000000..b550720 --- /dev/null +++ b/src/api/types.ts @@ -0,0 +1,55 @@ +export interface TagRecord { + name: string; + category: number; + post_count: number; + alias: string[]; + priority?: number; + matched_alias?: string; +} + +export interface TagsListResponse { + data: TagRecord[]; + total: number; +} + +export interface ApiStatusResponse { + status: string; + message?: string; +} + +export interface ToggleLikeResponse extends ApiStatusResponse { + is_liked: boolean; +} + +export interface CategoryRecord { + id: number; + name: string; + color: string; +} + +export interface PresetRecord { + id: string; + name: string; + tags: string[]; +} + +export interface PresetsResponse { + defaults: PresetRecord[]; + customs: PresetRecord[]; +} + +export interface UpdateCheckResponse { + update_available: boolean; + local_sha256: string; + remote_sha256: string; + version: string; + name: string; +} + +export interface AddTagsPayload { + name: string; + category: number; + post_count: number; + alias: string[]; + source?: string; +} diff --git a/src/components/CustomTagModal.vue b/src/components/CustomTagModal.vue index 8cef70c..ce601bf 100644 --- a/src/components/CustomTagModal.vue +++ b/src/components/CustomTagModal.vue @@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n'; import { Icon } from '@iconify/vue'; import { categoryService } from '../utils/categoryService'; import { TagCategory } from '../utils/types'; +import { simplePromptApi } from '../api/client'; const props = defineProps<{ visible: boolean; @@ -86,17 +87,7 @@ const handleSave = async () => { source: props.targetSource || 'user' }; - const response = await fetch('/simple-prompt/add-custom-tag', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); - - const result = await response.json(); - - if (!response.ok) { - throw new Error(result.error || response.statusText); - } + await simplePromptApi.addTag(payload); successMsg.value = props.editMode ? t('customTag.successEdited') || 'Tag updated!' : t('customTag.successAdded'); setTimeout(() => { diff --git a/src/components/SettingsModal.vue b/src/components/SettingsModal.vue index 8915eb3..1fbf564 100644 --- a/src/components/SettingsModal.vue +++ b/src/components/SettingsModal.vue @@ -6,6 +6,7 @@ import { settings } from '../utils/settings'; import TagManager from './TagManager.vue'; import { categoryService, type CategoryItem } from '../utils/categoryService'; import { metaService } from '../utils/metaService'; +import { simplePromptApi } from '../api/client'; const props = defineProps({ visible: { @@ -169,12 +170,7 @@ const handleDataUpdate = async (action: 'update_github' | 'update_liked' | 'upda updateStatus.value = t('settings.items.checkingUpdate'); try { - const checkResponse = await fetch('/simple-prompt/check-update'); - const checkResult = await checkResponse.json(); - - if (!checkResponse.ok) { - throw new Error(checkResult.error || checkResponse.statusText); - } + const checkResult = await simplePromptApi.checkUpdate(); latestVersion.value = checkResult.version; isChecking.value = false; @@ -186,19 +182,12 @@ const handleDataUpdate = async (action: 'update_github' | 'update_liked' | 'upda // 2. Proceed to update if available updateStatus.value = t('settings.items.updating'); + const updateResult = await simplePromptApi.updateTags(); - const updateResponse = await fetch('/simple-prompt/update-tags', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({}) - }); - - const updateResult = await updateResponse.json(); - - if (updateResponse.ok && updateResult.status === 'success') { + if (updateResult.status === 'success') { updateStatus.value = t('settings.items.updateSuccess'); } else { - throw new Error(updateResult.error || updateResponse.statusText); + throw new Error(updateResult.message || 'Update failed'); } } finally { isChecking.value = false; // ensure check flag cleared @@ -207,21 +196,12 @@ const handleDataUpdate = async (action: 'update_github' | 'update_liked' | 'upda else { // Local Data Update (Liked or User) updateStatus.value = t('settings.items.updating'); - const endpoint = '/simple-prompt/update-data'; - const payload = { action }; // 'update_liked' or 'update_user' + const result = await simplePromptApi.updateData(action); - const response = await fetch(endpoint, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); - - const result = await response.json(); - - if (response.ok && result.status === 'success') { + if (result.status === 'success') { updateStatus.value = result.message || t('settings.items.updateSuccess'); } else { - throw new Error(result.error || response.statusText); + throw new Error(result.message || 'Update failed'); } } diff --git a/src/components/TagManager.vue b/src/components/TagManager.vue index bd51a86..abae4dc 100644 --- a/src/components/TagManager.vue +++ b/src/components/TagManager.vue @@ -4,8 +4,9 @@ import { Icon } from '@iconify/vue'; import { useI18n } from 'vue-i18n'; import CustomTagModal from './CustomTagModal.vue'; import { settings } from '../utils/settings'; -import { type TagCategory, CATEGORY_COLORS } from '../utils/types'; // Keep for now if used, but prefer service import { categoryService } from '../utils/categoryService'; +import { simplePromptApi } from '../api/client'; +import type { TagRecord } from '../api/types'; const { t } = useI18n(); @@ -18,7 +19,7 @@ const tabs = computed(() => [ ]); // Data -const tags = ref([]); +const tags = ref([]); const totalTags = ref(0); const loading = ref(false); const searchQuery = ref(''); @@ -27,29 +28,16 @@ const pageSize = 20; // Edit Modal const showEditModal = ref(false); -const editingTag = ref(null); +const editingTag = ref(null); // Fetch Tags const fetchTags = async () => { loading.value = true; try { const offset = (currentPage.value - 1) * pageSize; - const params = new URLSearchParams({ - source: activeTab.value, - limit: pageSize.toString(), - offset: offset.toString(), - q: searchQuery.value - }); - - const response = await fetch(`/simple-prompt/tags/list?${params.toString()}`); - const result = await response.json(); - - if (response.ok) { - tags.value = result.data || []; - totalTags.value = result.total || 0; - } else { - console.error('Fetch tags failed:', result.error); - } + const result = await simplePromptApi.listTags(activeTab.value, pageSize, offset, searchQuery.value); + tags.value = result.data || []; + totalTags.value = result.total || 0; } catch (e) { console.error('Fetch tags error:', e); } finally { @@ -58,28 +46,18 @@ const fetchTags = async () => { }; // Actions -const handleDelete = async (tag: any) => { +const handleDelete = async (tag: TagRecord) => { if (!confirm(t('common.confirm') + ` Delete '${tag.name}'?`)) return; try { - const response = await fetch('/simple-prompt/tags/delete', { - method: 'DELETE', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: tag.name, source: activeTab.value }) - }); - - if (response.ok) { - fetchTags(); - } else { - const res = await response.json(); - alert('Delete failed: ' + res.error); - } + await simplePromptApi.deleteTag(tag.name, activeTab.value); + fetchTags(); } catch (e: any) { alert('Delete failed: ' + e.message); } }; -const handleEdit = (tag: any) => { +const handleEdit = (tag: TagRecord) => { editingTag.value = tag; showEditModal.value = true; }; @@ -134,21 +112,10 @@ const handleInlineAdd = async () => { source: activeTab.value })); - const response = await fetch('/simple-prompt/add-custom-tag', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ tags: tagsPayload }) - }); - - const res = await response.json(); - - if (response.ok) { - newTagName.value = ''; - fetchTags(); - // Show toast or alert? Just clean input is standard. - } else { - alert('Add failed: ' + res.error); - } + await simplePromptApi.addTags(tagsPayload); + newTagName.value = ''; + fetchTags(); + // Show toast or alert? Just clean input is standard. } catch (e: any) { alert('Add failed: ' + e.message); } finally { diff --git a/src/components/TagSearchModal.vue b/src/components/TagSearchModal.vue index ac5ec65..6a5832f 100644 --- a/src/components/TagSearchModal.vue +++ b/src/components/TagSearchModal.vue @@ -4,6 +4,8 @@ import { useI18n } from 'vue-i18n'; import { Icon } from '@iconify/vue'; import { DuckDBService } from '../utils/duckdbService'; import { CategoryService } from '../utils/categoryService'; +import { simplePromptApi } from '../api/client'; +import type { TagRecord } from '../api/types'; const props = defineProps<{ visible: boolean; @@ -17,7 +19,7 @@ const emit = defineEmits<{ }>(); const searchQuery = ref(''); -const searchResults = ref([]); +const searchResults = ref([]); const loading = ref(false); const selectedCategories = ref([]); @@ -89,7 +91,7 @@ const handleSearchInput = () => { }; // Add tag to prompt -const addTag = (tag: any) => { +const addTag = (tag: TagRecord) => { emit('add-tag', tag.name, tag.category); }; @@ -158,11 +160,11 @@ const getHighlightParts = (text: string, query: string): Array<{ text: string; m }; // --- Like Functionality --- -const isLiked = (tag: any) => { +const isLiked = (tag: TagRecord) => { return tag.priority === 1; // 1 = Liked source }; -const toggleLike = async (tag: any, event: Event) => { +const toggleLike = async (tag: TagRecord, event: Event) => { event.stopPropagation(); const currentlyLiked = isLiked(tag); @@ -178,11 +180,7 @@ const toggleLike = async (tag: any, event: Event) => { alias: tag.alias }; - await fetch('/simple-prompt/toggle-like-tag', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); + await simplePromptApi.toggleLikeTag(payload); // Success - no undo needed // Re-search to sort correctly if needed? diff --git a/src/utils/categoryService.ts b/src/utils/categoryService.ts index 75f6dd1..ff5ede8 100644 --- a/src/utils/categoryService.ts +++ b/src/utils/categoryService.ts @@ -1,5 +1,6 @@ import { TagCategory, CATEGORY_COLORS } from './types'; import { reactive } from 'vue'; +import { simplePromptApi } from '../api/client'; export interface CategoryData { value: number; @@ -51,21 +52,18 @@ export class CategoryService { public async fetchCategories(): Promise { try { - const response = await fetch('/simple-prompt/categories/list'); - if (response.ok) { - const data = await response.json(); + const data = await simplePromptApi.listCategories(); - if (Array.isArray(data)) { - // Clear custom ones if any, but keep defaults if fetch fails next time? - // Better to just update the map. - data.forEach((cat: any) => { - this._categories.set(cat.id, { - value: cat.id, - label: cat.name, - color: cat.color - }); + if (Array.isArray(data)) { + // Clear custom ones if any, but keep defaults if fetch fails next time? + // Better to just update the map. + data.forEach((cat) => { + this._categories.set(cat.id, { + value: cat.id, + label: cat.name, + color: cat.color }); - } + }); } } catch (error) { console.error("[CategoryService] Failed to fetch categories:", error); @@ -76,19 +74,9 @@ export class CategoryService { public async saveCustomCategories(categories: CategoryItem[]): Promise { try { - const response = await fetch('/simple-prompt/categories/save', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ categories }) - }); - - if (response.ok) { - // Refresh local state after save - await this.fetchCategories(); - } else { - console.error("[CategoryService] Failed to save categories:", await response.text()); - throw new Error("Failed to save categories"); - } + await simplePromptApi.saveCategories(categories); + // Refresh local state after save + await this.fetchCategories(); } catch (error) { console.error("[CategoryService] Error saving categories:", error); throw error; diff --git a/src/utils/duckdbService.ts b/src/utils/duckdbService.ts index c1a6a21..955ea20 100644 --- a/src/utils/duckdbService.ts +++ b/src/utils/duckdbService.ts @@ -1,3 +1,6 @@ +import { simplePromptApi } from '../api/client'; +import type { TagRecord } from '../api/types'; + export class DuckDBService { private static instance: DuckDBService; private isInitialized = false; @@ -20,24 +23,13 @@ export class DuckDBService { - public async searchTags(query: string, limit: number = 20, useAliases: boolean = false, categories: number[] = []): Promise { - const params = new URLSearchParams({ - q: query, - limit: limit.toString(), - use_aliases: useAliases.toString(), - categories: categories.join(',') - }); - + public async searchTags(query: string, limit: number = 20, useAliases: boolean = false, categories: number[] = []): Promise { try { - const response = await fetch(`/simple-prompt/search-tags?${params.toString()}`); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const results = await response.json(); + const results = await simplePromptApi.searchTags(query, limit, useAliases, categories); // Handle the same alias matching logic as before for UI highlighting if (useAliases && results.length > 0) { - return results.map((row: any) => { + return results.map((row) => { const nameMatches = row.name && row.name.toLowerCase().includes(query.toLowerCase()); // If name matches, it's a direct match @@ -68,22 +60,11 @@ export class DuckDBService { } } - public async getTagsDetails(names: string[]): Promise> { + public async getTagsDetails(names: string[], fast = false): Promise> { if (names.length === 0) return {}; try { - const response = await fetch('/simple-prompt/get-tags-details', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ names }) - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return await response.json(); + return await simplePromptApi.getTagsDetails(names, fast); } catch (error) { console.error("[DuckDB] Get tags details failed:", error); return {}; diff --git a/src/utils/metaService.ts b/src/utils/metaService.ts index 693a960..b2198ea 100644 --- a/src/utils/metaService.ts +++ b/src/utils/metaService.ts @@ -1,11 +1,13 @@ import { ref } from 'vue'; +import { simplePromptApi } from '../api/client'; +import type { PresetRecord } from '../api/types'; class MetaService { private static instance: MetaService; // Preset support - public presets = ref([]); // { id, name, tags } - public defaultPresets = ref([]); - public customPresets = ref([]); + public presets = ref([]); + public defaultPresets = ref([]); + public customPresets = ref([]); public activePresetId = ref(''); public loading = ref(false); @@ -24,35 +26,27 @@ class MetaService { public async fetchPresets() { try { - const response = await fetch('/simple-prompt/presets/list'); - if (response.ok) { - const data = await response.json(); - this.defaultPresets.value = data.defaults || []; - this.customPresets.value = data.customs || []; + const data = await simplePromptApi.listPresets(); + this.defaultPresets.value = data.defaults || []; + this.customPresets.value = data.customs || []; - // Merge - this.presets.value = [...this.defaultPresets.value, ...this.customPresets.value]; + // Merge + this.presets.value = [...this.defaultPresets.value, ...this.customPresets.value]; - // Restore active preset logic or default to first if none - if (!this.activePresetId.value && this.presets.value.length > 0) { - // Check storage? - // For now default to first - this.activePresetId.value = this.presets.value[0].id; - } + // Restore active preset logic or default to first if none + if (!this.activePresetId.value && this.presets.value.length > 0) { + // Check storage? + // For now default to first + this.activePresetId.value = this.presets.value[0].id; } } catch (e) { console.error("Error fetching presets:", e); } } - public async saveCustomPresets(customs: any[]) { + public async saveCustomPresets(customs: PresetRecord[]) { try { - const response = await fetch('/simple-prompt/presets/save', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ presets: customs }) - }); - if (!response.ok) throw new Error("Failed"); + await simplePromptApi.savePresets(customs); await this.fetchPresets(); } catch (e) { console.error("Error saving presets:", e);