feat: 新增 BbSelect 组件,实现下拉选择功能
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BrowserBookmark Options</title>
|
||||
<title>云书签 选项</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -2,7 +2,7 @@ const BASE_URL = import.meta.env.VITE_SERVER_BASE_URL || "http://localhost:3001"
|
||||
|
||||
export async function apiFetch(path, options = {}) {
|
||||
const headers = new Headers(options.headers || {});
|
||||
headers.set("Accept", "application/json");
|
||||
if (!headers.has("Accept")) headers.set("Accept", "application/json");
|
||||
|
||||
if (!(options.body instanceof FormData) && options.body != null) {
|
||||
headers.set("Content-Type", "application/json");
|
||||
|
||||
@@ -5,7 +5,7 @@ import { RouterLink, RouterView } from "vue-router";
|
||||
<template>
|
||||
<div class="shell">
|
||||
<header class="nav">
|
||||
<div class="brand">BrowserBookmark 扩展</div>
|
||||
<div class="brand">云书签</div>
|
||||
<nav class="links">
|
||||
<RouterLink to="/" class="link">公开</RouterLink>
|
||||
<RouterLink to="/my" class="link">我的</RouterLink>
|
||||
|
||||
@@ -8,6 +8,10 @@ const file = ref(null);
|
||||
const status = ref("");
|
||||
const error = ref("");
|
||||
|
||||
function onFileChange(e) {
|
||||
file.value = e?.target?.files?.[0] || null;
|
||||
}
|
||||
|
||||
async function importToLocal() {
|
||||
status.value = "";
|
||||
error.value = "";
|
||||
@@ -44,8 +48,30 @@ async function importFile() {
|
||||
}
|
||||
}
|
||||
|
||||
function exportCloud() {
|
||||
window.open("http://localhost:3001/bookmarks/export/html", "_blank");
|
||||
async function exportCloud() {
|
||||
status.value = "";
|
||||
error.value = "";
|
||||
|
||||
try {
|
||||
const html = await apiFetch("/bookmarks/export/html", {
|
||||
method: "GET",
|
||||
headers: { Accept: "text/html" }
|
||||
});
|
||||
|
||||
const blob = new Blob([html], { type: "text/html;charset=utf-8" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "bookmarks-cloud.html";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
status.value = "云端导出完成";
|
||||
} catch (e) {
|
||||
error.value = e.message || String(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function exportLocal() {
|
||||
@@ -82,7 +108,7 @@ async function exportLocal() {
|
||||
<input
|
||||
type="file"
|
||||
accept="text/html,.html"
|
||||
@change="(e) => (file.value = e.target.files?.[0] || null)"
|
||||
@change="onFileChange"
|
||||
/>
|
||||
<button class="btn" @click="importFile">开始导入</button>
|
||||
<p v-if="status" class="ok">{{ status }}</p>
|
||||
|
||||
@@ -12,6 +12,16 @@ const mode = ref("local");
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
|
||||
const cloudAvailable = ref(false);
|
||||
|
||||
const addCurrentOpen = ref(false);
|
||||
const foldersLoading = ref(false);
|
||||
const folders = ref([]);
|
||||
const selectedFolderId = ref(null);
|
||||
const addCurrentBusy = ref(false);
|
||||
const addCurrentStatus = ref("");
|
||||
const activePage = ref({ title: "", url: "" });
|
||||
|
||||
const filtered = computed(() => {
|
||||
const query = q.value.trim().toLowerCase();
|
||||
if (!query) return items.value;
|
||||
@@ -36,11 +46,89 @@ function openOptions() {
|
||||
}
|
||||
}
|
||||
|
||||
async function getActiveTabPage() {
|
||||
try {
|
||||
if (typeof chrome !== "undefined" && chrome.tabs?.query) {
|
||||
const tabs = await new Promise((resolve) => {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (result) => resolve(result || []));
|
||||
});
|
||||
const tab = tabs?.[0];
|
||||
return { title: tab?.title || "", url: tab?.url || "" };
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return { title: document.title || "", url: window.location?.href || "" };
|
||||
}
|
||||
|
||||
async function loadFolders() {
|
||||
foldersLoading.value = true;
|
||||
try {
|
||||
const list = await apiFetch("/folders");
|
||||
folders.value = Array.isArray(list) ? list : [];
|
||||
} finally {
|
||||
foldersLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function openAddCurrent() {
|
||||
addCurrentStatus.value = "";
|
||||
error.value = "";
|
||||
selectedFolderId.value = null;
|
||||
activePage.value = await getActiveTabPage();
|
||||
|
||||
const token = await getToken();
|
||||
cloudAvailable.value = Boolean(token);
|
||||
if (!token) {
|
||||
addCurrentOpen.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
addCurrentOpen.value = true;
|
||||
await loadFolders();
|
||||
}
|
||||
|
||||
async function addCurrentToCloud() {
|
||||
addCurrentStatus.value = "";
|
||||
error.value = "";
|
||||
|
||||
const token = await getToken();
|
||||
if (!token) {
|
||||
error.value = "请先登录后再添加到云书签";
|
||||
return;
|
||||
}
|
||||
|
||||
const page = await getActiveTabPage();
|
||||
const t = String(page.title || "").trim() || String(page.url || "").trim();
|
||||
const u = String(page.url || "").trim();
|
||||
if (!u) return;
|
||||
|
||||
try {
|
||||
addCurrentBusy.value = true;
|
||||
await apiFetch("/bookmarks", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
folderId: selectedFolderId.value ?? null,
|
||||
title: t,
|
||||
url: u,
|
||||
visibility: "private"
|
||||
})
|
||||
});
|
||||
addCurrentStatus.value = "已添加到云书签";
|
||||
await load();
|
||||
} catch (e) {
|
||||
error.value = e.message || String(e);
|
||||
} finally {
|
||||
addCurrentBusy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
const token = await getToken();
|
||||
cloudAvailable.value = Boolean(token);
|
||||
mode.value = token ? "cloud" : "local";
|
||||
|
||||
if (mode.value === "cloud") {
|
||||
@@ -109,6 +197,37 @@ onMounted(load);
|
||||
<button class="btn primary" @click="quickAdd">添加</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="cardTitle">添加当前页面到云书签</div>
|
||||
<div class="muted">只需选一个文件夹(不选默认根目录)。</div>
|
||||
<button class="btn primary" :disabled="addCurrentBusy" @click="openAddCurrent">添加当前页面</button>
|
||||
|
||||
<div v-if="addCurrentOpen" class="subCard">
|
||||
<div class="subTitle">当前页面</div>
|
||||
<div class="subText">{{ activePage.title || '(无标题)' }}</div>
|
||||
<div class="subUrl">{{ activePage.url || '(无法获取链接)' }}</div>
|
||||
|
||||
<template v-if="cloudAvailable">
|
||||
<div class="subTitle" style="margin-top: 10px;">选择文件夹</div>
|
||||
<select v-model="selectedFolderId" class="input" :disabled="foldersLoading || addCurrentBusy">
|
||||
<option :value="null">(根目录)</option>
|
||||
<option v-for="f in folders" :key="f.id" :value="f.id">{{ f.name }}</option>
|
||||
</select>
|
||||
<button class="btn primary" :disabled="addCurrentBusy || !activePage.url" @click="addCurrentToCloud">
|
||||
{{ addCurrentBusy ? '添加中…' : '确认添加到云端' }}
|
||||
</button>
|
||||
<div v-if="foldersLoading" class="muted">加载文件夹中…</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="muted" style="margin-top: 10px;">当前未登录,无法添加到云端。</div>
|
||||
<button class="btn" @click="openOptions">去登录</button>
|
||||
</template>
|
||||
|
||||
<div v-if="addCurrentStatus" class="ok">{{ addCurrentStatus }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error">{{ error }}</div>
|
||||
<div v-if="loading" class="muted">加载中…</div>
|
||||
|
||||
@@ -138,6 +257,11 @@ onMounted(load);
|
||||
.input { padding: 8px 10px; border: 1px solid #e5e7eb; border-radius: 10px; background: white; }
|
||||
.card { border: 1px solid #e5e7eb; border-radius: 12px; padding: 10px; background: white; display: grid; gap: 8px; margin-bottom: 10px; }
|
||||
.cardTitle { font-weight: 700; font-size: 12px; color: #334155; }
|
||||
.subCard { border: 1px dashed #e5e7eb; border-radius: 12px; padding: 10px; background: #f8fafc; display: grid; gap: 8px; }
|
||||
.subTitle { font-weight: 700; font-size: 12px; color: #334155; }
|
||||
.subText { font-weight: 700; color: #0f172a; overflow-wrap: anywhere; }
|
||||
.subUrl { font-size: 12px; color: #475569; overflow-wrap: anywhere; }
|
||||
.ok { font-size: 12px; color: #065f46; background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 10px; padding: 8px 10px; }
|
||||
.list { list-style: none; padding: 0; margin: 0; display: grid; gap: 8px; }
|
||||
.item { border: 1px solid #e5e7eb; border-radius: 12px; padding: 10px; cursor: pointer; background: white; transition: background 150ms, border-color 150ms; }
|
||||
.item:hover { background: rgba(59, 130, 246, 0.06); border-color: rgba(59, 130, 246, 0.35); }
|
||||
|
||||
Reference in New Issue
Block a user