You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

217 lines
6.8 KiB

<template>
<div class="modal-overlay" :class="{ open }" @click.self="$emit('close')">
<div class="modal">
<div class="modal-title">新增收藏</div>
<div class="form-group">
<label class="form-label">内容类型</label>
<div class="type-selector">
<div
v-for="opt in typeOptions"
:key="opt.value"
class="type-opt"
:class="{ selected: form.type === opt.value }"
@click="form.type = opt.value"
>{{ opt.emoji }} {{ opt.label }}</div>
</div>
</div>
<div class="form-group">
<label class="form-label">链接或内容</label>
<input class="form-input" type="text" placeholder="https://... 或粘贴文字" v-model="form.url" @blur="onUrlBlur">
<div v-if="urlFetching" class="text-xs mt-1" style="color:var(--accent)">正在抓取元数据...</div>
</div>
<div class="form-group">
<label class="form-label">标题{{ form.title ? '' : '(留空自动识别)' }}</label>
<input class="form-input" type="text" placeholder="给这条收藏起个名字" v-model="form.title">
</div>
<div class="form-group">
<label class="form-label">分类</label>
<select class="form-input" v-model="form.categoryId">
<option :value="undefined">无分类(收件箱)</option>
<option v-for="cat in categoryOptions" :key="cat.id" :value="cat.id">{{ cat.icon }} {{ cat.name }}</option>
</select>
</div>
<div class="form-group">
<label class="form-label">标签(空格分隔)</label>
<input class="form-input" type="text" placeholder="灵感 设计 AI工具" v-model="form.tagsRaw">
</div>
<div class="form-group">
<label class="form-label">备注</label>
<textarea class="form-input" placeholder="写下你的想法..." v-model="form.note" rows="3"></textarea>
</div>
<div class="modal-actions">
<button class="btn-cancel" @click="$emit('close')">取消</button>
<button class="btn-save" @click="submit" :disabled="saving">
{{ saving ? '保存中...' : '保存收藏' }}
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{ open: boolean }>();
const emit = defineEmits<{ close: []; save: [data: any] }>();
const categories = useCategoriesStore();
const saving = ref(false);
const urlFetching = ref(false);
const form = reactive({
type: 'web' as string,
url: '',
title: '',
categoryId: undefined as number | undefined,
tagsRaw: '',
note: '',
});
const categoryOptions = computed(() => {
const flat: any[] = [];
function walk(nodes: any[]) {
for (const n of nodes) {
flat.push(n);
if (n.children?.length) walk(n.children);
}
}
walk(categories.tree);
return flat;
});
const typeOptions = [
{ value: 'web', emoji: '🌐', label: '网页' },
{ value: 'text', emoji: '📝', label: '文本' },
{ value: 'image', emoji: '🖼️', label: '图片' },
{ value: 'video', emoji: '🎬', label: '视频' },
{ value: 'file', emoji: '📄', label: '文档' },
];
let urlTimer: any;
async function onUrlBlur() {
if (!form.url || !form.url.startsWith('http')) return;
urlFetching.value = true;
try {
const res = await $fetch<any>('/api/items/fetch-url', {
method: 'POST',
body: { url: form.url },
});
if (res.code === 0 && res.data) {
if (!form.title) form.title = res.data.title || '';
}
} catch { /* ignore */ }
urlFetching.value = false;
}
async function submit() {
if (!form.title.trim() && !form.url.trim()) return;
saving.value = true;
const data = {
type: form.type,
title: form.title.trim() || (form.url ? form.url.replace(/^https?:\/\//, '').split('/')[0] : '新收藏'),
url: form.url.trim() || undefined,
categoryId: form.categoryId,
tagNames: form.tagsRaw.trim() ? form.tagsRaw.trim().split(/\s+/).filter(Boolean) : [],
note: form.note.trim() || undefined,
};
emit('save', data);
// Reset form
form.url = '';
form.title = '';
form.categoryId = undefined;
form.tagsRaw = '';
form.note = '';
form.type = 'web';
saving.value = false;
}
// Reset on close
watch(() => props.open, (v) => {
if (v && categories.tree.length === 0) {
categories.fetchCategories();
}
if (!v) {
form.url = '';
form.title = '';
form.categoryId = undefined;
form.tagsRaw = '';
form.note = '';
}
});
</script>
<style scoped>
.modal-overlay {
position: fixed; inset: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
opacity: 0; pointer-events: none;
transition: opacity 0.2s;
}
.modal-overlay.open { opacity: 1; pointer-events: all; }
.modal {
background: var(--bg2);
border: 1px solid var(--border2);
border-radius: var(--radius-lg);
width: 480px;
max-width: 90vw;
padding: 24px;
transform: translateY(10px) scale(0.98);
transition: transform 0.2s;
}
.modal-overlay.open .modal { transform: translateY(0) scale(1); }
.modal-title {
font-family: var(--font-display);
font-size: 18px;
color: var(--text);
margin-bottom: 20px;
}
.form-group { margin-bottom: 16px; }
.form-label { font-size: 12px; color: var(--text2); margin-bottom: 6px; display: block; }
.form-input {
width: 100%;
background: var(--bg3);
border: 1px solid var(--border);
border-radius: 8px;
padding: 9px 12px;
color: var(--text);
font-family: var(--font-body);
font-size: 13px;
outline: none;
transition: border-color 0.2s;
}
.form-input:focus { border-color: rgba(200, 169, 126, 0.4); }
.form-input::placeholder { color: var(--text3); }
textarea.form-input { resize: vertical; min-height: 80px; line-height: 1.6; }
.type-selector { display: flex; gap: 6px; flex-wrap: wrap; }
.type-opt {
padding: 6px 12px; border-radius: 8px; font-size: 12px;
border: 1px solid var(--border); cursor: pointer;
color: var(--text2); transition: all 0.15s;
}
.type-opt.selected { border-color: rgba(200, 169, 126, 0.4); background: var(--accent-bg); color: var(--accent); }
.type-opt:hover:not(.selected) { border-color: var(--border2); }
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 20px; }
.btn-cancel {
padding: 8px 16px; border-radius: 8px; font-family: var(--font-body);
font-size: 13px; cursor: pointer; border: 1px solid var(--border);
background: transparent; color: var(--text2); transition: all 0.15s;
}
.btn-cancel:hover { border-color: var(--border2); color: var(--text); }
.btn-save {
padding: 8px 20px; border-radius: 8px; font-family: var(--font-body);
font-size: 13px; font-weight: 500; cursor: pointer; border: none;
background: var(--accent); color: #1a1208; transition: all 0.15s;
}
.btn-save:hover { background: #d4b88a; }
.btn-save:disabled { opacity: 0.6; cursor: default; }
</style>