增加收藏和文件夹分享功能
This commit is contained in:
@@ -90,7 +90,9 @@ const filteredPresets = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Filter by Folder
|
// Filter by Folder
|
||||||
if (selectedFolderId.value) {
|
if (selectedFolderId.value === 'favorites') {
|
||||||
|
presets = presets.filter(p => p.isFavorite);
|
||||||
|
} else if (selectedFolderId.value) {
|
||||||
presets = presets.filter(p => p.folderId === selectedFolderId.value);
|
presets = presets.filter(p => p.folderId === selectedFolderId.value);
|
||||||
} else if (selectedFolderId.value === '') {
|
} else if (selectedFolderId.value === '') {
|
||||||
// Uncategorized
|
// Uncategorized
|
||||||
@@ -137,6 +139,7 @@ const flattenedFolders = computed(() => {
|
|||||||
|
|
||||||
const allPresetsCount = computed(() => (store.extendedPresets || []).length);
|
const allPresetsCount = computed(() => (store.extendedPresets || []).length);
|
||||||
const uncategorizedCount = computed(() => (store.extendedPresets || []).filter(p => !p.folderId).length);
|
const uncategorizedCount = computed(() => (store.extendedPresets || []).filter(p => !p.folderId).length);
|
||||||
|
const favoritesCount = computed(() => (store.extendedPresets || []).filter(p => p.isFavorite).length);
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
function handleFolderSelect(id: string | null) {
|
function handleFolderSelect(id: string | null) {
|
||||||
@@ -262,6 +265,13 @@ function deletePreset(preset: ExtendedPreset) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleFavorite(preset: ExtendedPreset) {
|
||||||
|
store.updateExtendedPreset(preset.id, { isFavorite: !preset.isFavorite });
|
||||||
|
if (!preset.isFavorite) {
|
||||||
|
showNotification(`已添加到收藏`, 'success');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function copyPresetContent(preset: ExtendedPreset) {
|
async function copyPresetContent(preset: ExtendedPreset) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(preset.content);
|
await navigator.clipboard.writeText(preset.content);
|
||||||
@@ -327,9 +337,11 @@ const shareLoading = ref(false);
|
|||||||
const shareResultCode = ref('');
|
const shareResultCode = ref('');
|
||||||
const shareImportCode = ref('');
|
const shareImportCode = ref('');
|
||||||
const shareSinglePreset = ref<ExtendedPreset | null>(null);
|
const shareSinglePreset = ref<ExtendedPreset | null>(null);
|
||||||
|
const shareFolder = ref<PresetFolder | null>(null);
|
||||||
|
|
||||||
function openShareDialog(preset?: ExtendedPreset) {
|
function openShareDialog(preset?: ExtendedPreset, folder?: PresetFolder) {
|
||||||
shareSinglePreset.value = preset || null;
|
shareSinglePreset.value = preset || null;
|
||||||
|
shareFolder.value = folder || null;
|
||||||
shareTab.value = 'create';
|
shareTab.value = 'create';
|
||||||
shareResultCode.value = '';
|
shareResultCode.value = '';
|
||||||
shareImportCode.value = '';
|
shareImportCode.value = '';
|
||||||
@@ -340,6 +352,10 @@ function handleShare(preset: ExtendedPreset) {
|
|||||||
openShareDialog(preset);
|
openShareDialog(preset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleShareFolder(folder: PresetFolder) {
|
||||||
|
openShareDialog(undefined, folder);
|
||||||
|
}
|
||||||
|
|
||||||
async function generateShareCode() {
|
async function generateShareCode() {
|
||||||
shareLoading.value = true;
|
shareLoading.value = true;
|
||||||
try {
|
try {
|
||||||
@@ -349,6 +365,33 @@ async function generateShareCode() {
|
|||||||
if (shareSinglePreset.value) {
|
if (shareSinglePreset.value) {
|
||||||
data = shareSinglePreset.value;
|
data = shareSinglePreset.value;
|
||||||
type = 'single';
|
type = 'single';
|
||||||
|
} else if (shareFolder.value) {
|
||||||
|
// Find all presets in this folder and its subfolders?
|
||||||
|
// User likely just wants this folder and its direct contents, OR the whole tree.
|
||||||
|
// Simplest is recursive export of folder + subfolders + presets inside them.
|
||||||
|
|
||||||
|
const folderIds = new Set<string>();
|
||||||
|
const foldersToExport: PresetFolder[] = [];
|
||||||
|
|
||||||
|
const collectFolders = (id: string) => {
|
||||||
|
const folder = store.presetFolders.find(f => f.id === id);
|
||||||
|
if (folder) {
|
||||||
|
folderIds.add(id);
|
||||||
|
foldersToExport.push(folder);
|
||||||
|
// Find subfolders
|
||||||
|
store.presetFolders.filter(f => f.parentId === id).forEach(f => collectFolders(f.id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
collectFolders(shareFolder.value.id);
|
||||||
|
|
||||||
|
const presetsToExport = store.extendedPresets.filter(p => p.folderId && folderIds.has(p.folderId));
|
||||||
|
|
||||||
|
data = {
|
||||||
|
presetFolders: foldersToExport,
|
||||||
|
extendedPresets: presetsToExport
|
||||||
|
};
|
||||||
|
type = 'folder';
|
||||||
} else {
|
} else {
|
||||||
const jsonString = store.exportPresetsToJson();
|
const jsonString = store.exportPresetsToJson();
|
||||||
try {
|
try {
|
||||||
@@ -594,6 +637,7 @@ function closeShareDialog() {
|
|||||||
shareResultCode.value = '';
|
shareResultCode.value = '';
|
||||||
shareImportCode.value = '';
|
shareImportCode.value = '';
|
||||||
shareSinglePreset.value = null;
|
shareSinglePreset.value = null;
|
||||||
|
shareFolder.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
@@ -648,12 +692,14 @@ onMounted(() => {
|
|||||||
:expanded-ids="expandedFolderIds"
|
:expanded-ids="expandedFolderIds"
|
||||||
:all-count="allPresetsCount"
|
:all-count="allPresetsCount"
|
||||||
:uncategorized-count="uncategorizedCount"
|
:uncategorized-count="uncategorizedCount"
|
||||||
|
:favorites-count="favoritesCount"
|
||||||
@update:selected-folder-id="handleFolderSelect"
|
@update:selected-folder-id="handleFolderSelect"
|
||||||
@toggle-expand="handleToggleExpand"
|
@toggle-expand="handleToggleExpand"
|
||||||
@create-folder="createFolder()"
|
@create-folder="createFolder()"
|
||||||
@create-sub-folder="createFolder"
|
@create-sub-folder="createFolder"
|
||||||
@edit-folder="editFolder"
|
@edit-folder="editFolder"
|
||||||
@delete-folder="deleteFolder"
|
@delete-folder="deleteFolder"
|
||||||
|
@share-folder="handleShareFolder"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -721,14 +767,15 @@ onMounted(() => {
|
|||||||
|
|
||||||
<div class="pm-content-area">
|
<div class="pm-content-area">
|
||||||
<PresetList
|
<PresetList
|
||||||
:presets="filteredPresets"
|
:presets="filteredPresets"
|
||||||
:search-query="searchQuery"
|
:search-query="searchQuery"
|
||||||
@apply="applyPreset"
|
@apply="applyPreset"
|
||||||
@edit="editPreset"
|
@edit="editPreset"
|
||||||
@delete="deletePreset"
|
@delete="deletePreset"
|
||||||
@copy="copyPresetContent"
|
@copy="copyPresetContent"
|
||||||
@share="handleShare"
|
@share="handleShare"
|
||||||
/>
|
@toggle-favorite="toggleFavorite"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -867,6 +914,9 @@ onMounted(() => {
|
|||||||
<p v-if="shareSinglePreset">
|
<p v-if="shareSinglePreset">
|
||||||
正在分享预设: <strong>{{ shareSinglePreset.name }}</strong>
|
正在分享预设: <strong>{{ shareSinglePreset.name }}</strong>
|
||||||
</p>
|
</p>
|
||||||
|
<p v-else-if="shareFolder">
|
||||||
|
正在分享文件夹: <strong>{{ shareFolder.name }}</strong>
|
||||||
|
</p>
|
||||||
<p v-else>
|
<p v-else>
|
||||||
正在分享: <strong>所有预设数据</strong>
|
正在分享: <strong>所有预设数据</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const emit = defineEmits<{
|
|||||||
(e: 'create-sub', parentId: string): void;
|
(e: 'create-sub', parentId: string): void;
|
||||||
(e: 'edit', folder: PresetFolder): void;
|
(e: 'edit', folder: PresetFolder): void;
|
||||||
(e: 'delete', folder: PresetFolder): void;
|
(e: 'delete', folder: PresetFolder): void;
|
||||||
|
(e: 'share', folder: PresetFolder): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const isExpanded = computed(() => props.expandedIds.has(props.folder.id));
|
const isExpanded = computed(() => props.expandedIds.has(props.folder.id));
|
||||||
@@ -85,6 +86,15 @@ const showActions = ref(false);
|
|||||||
|
|
||||||
<!-- 操作按钮组 (悬停显示) -->
|
<!-- 操作按钮组 (悬停显示) -->
|
||||||
<div class="folder-actions" v-show="showActions || isSelected">
|
<div class="folder-actions" v-show="showActions || isSelected">
|
||||||
|
<button @click.stop="emit('share', folder)" title="分享文件夹">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="18" cy="5" r="3"/>
|
||||||
|
<circle cx="6" cy="12" r="3"/>
|
||||||
|
<circle cx="18" cy="19" r="3"/>
|
||||||
|
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/>
|
||||||
|
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button @click.stop="emit('create-sub', folder.id)" title="新建子文件夹">
|
<button @click.stop="emit('create-sub', folder.id)" title="新建子文件夹">
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M12 5v14M5 12h14"/>
|
<path d="M12 5v14M5 12h14"/>
|
||||||
@@ -118,6 +128,7 @@ const showActions = ref(false);
|
|||||||
@create-sub="emit('create-sub', $event)"
|
@create-sub="emit('create-sub', $event)"
|
||||||
@edit="emit('edit', $event)"
|
@edit="emit('edit', $event)"
|
||||||
@delete="emit('delete', $event)"
|
@delete="emit('delete', $event)"
|
||||||
|
@share="emit('share', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const emit = defineEmits<{
|
|||||||
(e: 'delete', preset: ExtendedPreset): void;
|
(e: 'delete', preset: ExtendedPreset): void;
|
||||||
(e: 'copy', preset: ExtendedPreset): void;
|
(e: 'copy', preset: ExtendedPreset): void;
|
||||||
(e: 'share', preset: ExtendedPreset): void;
|
(e: 'share', preset: ExtendedPreset): void;
|
||||||
|
(e: 'toggle-favorite', preset: ExtendedPreset): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
function getTypeIcon(type: PresetType) {
|
function getTypeIcon(type: PresetType) {
|
||||||
@@ -61,6 +62,11 @@ function formatDate(dateStr: string) {
|
|||||||
</div>
|
</div>
|
||||||
<h4 class="preset-name" :title="preset.name">{{ preset.name }}</h4>
|
<h4 class="preset-name" :title="preset.name">{{ preset.name }}</h4>
|
||||||
<div class="preset-actions">
|
<div class="preset-actions">
|
||||||
|
<button @click="emit('toggle-favorite', preset)" class="action-btn" :class="{ 'is-favorite': preset.isFavorite }" title="收藏">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" :fill="preset.isFavorite ? 'currentColor' : 'none'" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button @click="emit('apply', preset)" class="action-btn apply-btn" title="应用预设">
|
<button @click="emit('apply', preset)" class="action-btn apply-btn" title="应用预设">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<polyline points="20,6 9,17 4,12"/>
|
<polyline points="20,6 9,17 4,12"/>
|
||||||
@@ -240,6 +246,17 @@ function formatDate(dateStr: string) {
|
|||||||
border-color: var(--color-accent);
|
border-color: var(--color-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.is-favorite {
|
||||||
|
color: #ef4444;
|
||||||
|
border-color: rgba(239, 68, 68, 0.3);
|
||||||
|
background-color: rgba(239, 68, 68, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-favorite:hover {
|
||||||
|
background-color: rgba(239, 68, 68, 0.2);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
/* Dropdown menu implementation */
|
/* Dropdown menu implementation */
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const props = defineProps<{
|
|||||||
expandedIds: Set<string>;
|
expandedIds: Set<string>;
|
||||||
allCount: number;
|
allCount: number;
|
||||||
uncategorizedCount: number;
|
uncategorizedCount: number;
|
||||||
|
favoritesCount: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -23,6 +24,7 @@ const emit = defineEmits<{
|
|||||||
(e: 'create-sub-folder', parentId: string): void;
|
(e: 'create-sub-folder', parentId: string): void;
|
||||||
(e: 'edit-folder', folder: PresetFolder): void;
|
(e: 'edit-folder', folder: PresetFolder): void;
|
||||||
(e: 'delete-folder', folder: PresetFolder): void;
|
(e: 'delete-folder', folder: PresetFolder): void;
|
||||||
|
(e: 'share-folder', folder: PresetFolder): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
function selectFolder(id: string | null) {
|
function selectFolder(id: string | null) {
|
||||||
@@ -55,6 +57,16 @@ function selectFolder(id: string | null) {
|
|||||||
<span class="item-name">所有预设</span>
|
<span class="item-name">所有预设</span>
|
||||||
<span class="item-count">{{ allCount }}</span>
|
<span class="item-count">{{ allCount }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="sidebar-item"
|
||||||
|
:class="{ active: selectedFolderId === 'favorites' }"
|
||||||
|
@click="selectFolder('favorites')"
|
||||||
|
>
|
||||||
|
<span class="item-icon">❤️</span>
|
||||||
|
<span class="item-name">我的收藏</span>
|
||||||
|
<span class="item-count">{{ favoritesCount }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="sidebar-item"
|
class="sidebar-item"
|
||||||
@@ -83,6 +95,7 @@ function selectFolder(id: string | null) {
|
|||||||
@create-sub="emit('create-sub-folder', $event)"
|
@create-sub="emit('create-sub-folder', $event)"
|
||||||
@edit="emit('edit-folder', $event)"
|
@edit="emit('edit-folder', $event)"
|
||||||
@delete="emit('delete-folder', $event)"
|
@delete="emit('delete-folder', $event)"
|
||||||
|
@share="emit('share-folder', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export interface ExtendedPreset {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
isPublic?: boolean;
|
isPublic?: boolean;
|
||||||
author?: string;
|
author?: string;
|
||||||
|
isFavorite?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 预设文件夹
|
// 预设文件夹
|
||||||
|
|||||||
Reference in New Issue
Block a user