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
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>
|
|
|