Files
Xu_BrowserBookmark/apps/extension/src/options/pages/MyPage.vue

135 lines
4.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { computed, onMounted, ref } from "vue";
import { apiFetch } from "../../lib/api";
import { getToken } from "../../lib/extStorage";
import { listLocalBookmarks, markLocalDeleted, upsertLocalBookmark } from "../../lib/localData";
import BbConfirmModal from "../../components/BbConfirmModal.vue";
const token = ref("");
const loggedIn = computed(() => Boolean(token.value));
const items = ref([]);
const error = ref("");
const mode = computed(() => (loggedIn.value ? "cloud" : "local"));
const title = ref("");
const url = ref("");
async function load() {
error.value = "";
try {
token.value = await getToken();
if (!token.value) {
items.value = await listLocalBookmarks();
return;
}
items.value = await apiFetch("/bookmarks");
} catch (e) {
error.value = e.message || String(e);
}
}
async function add() {
if (!title.value || !url.value) return;
if (mode.value === "cloud") {
await apiFetch("/bookmarks", {
method: "POST",
body: JSON.stringify({ folderId: null, title: title.value, url: url.value, visibility: "public" })
});
} else {
await upsertLocalBookmark({ title: title.value, url: url.value, visibility: "public" });
}
title.value = "";
url.value = "";
await load();
}
async function remove(id) {
if (mode.value !== "local") return;
pendingDeleteId.value = id;
deleteConfirmOpen.value = true;
}
const deleteConfirmOpen = ref(false);
const pendingDeleteId = ref("");
async function confirmDelete() {
const id = pendingDeleteId.value;
if (!id) {
deleteConfirmOpen.value = false;
return;
}
await markLocalDeleted(id);
pendingDeleteId.value = "";
deleteConfirmOpen.value = false;
await load();
}
function cancelDelete() {
pendingDeleteId.value = "";
deleteConfirmOpen.value = false;
}
onMounted(load);
</script>
<template>
<section>
<h1>我的书签{{ mode === 'cloud' ? '云端' : '本地' }}</h1>
<p v-if="!loggedIn" class="muted">未登录时书签保存在扩展本地可在登录后自动合并上云</p>
<div class="form">
<input v-model="title" class="input" placeholder="标题" />
<input v-model="url" class="input" placeholder="链接" @keydown.enter.prevent="add" />
<button class="btn" @click="add">添加</button>
</div>
<p v-if="error" class="error">{{ error }}</p>
<ul class="list">
<li v-for="b in items" :key="b.id" class="card">
<div class="row">
<a :href="b.url" target="_blank" rel="noopener" class="title">{{ b.title }}</a>
<button v-if="mode === 'local'" class="ghost" @click.prevent="remove(b.id)">删除</button>
</div>
<div class="muted">{{ b.url }}</div>
</li>
</ul>
<BbConfirmModal
v-model="deleteConfirmOpen"
title="删除书签"
message="确定删除该书签?"
confirm-text="删除"
cancel-text="取消"
:danger="true"
@confirm="confirmDelete"
@cancel="cancelDelete"
/>
</section>
</template>
<style scoped>
.form { display: grid; gap: 10px; grid-template-columns: 1fr; margin: 12px 0; }
@media (min-width: 900px) { .form { grid-template-columns: 2fr 3fr auto; } }
.input { padding: 10px 12px; border: 1px solid var(--bb-border); border-radius: 10px; background: var(--bb-panel); color: var(--bb-text); }
.btn { padding: 10px 12px; border: 1px solid rgba(255,255,255,0.15); border-radius: 10px; background: linear-gradient(135deg, var(--bb-primary), var(--bb-cta)); color: white; cursor: pointer; }
.row { display: flex; justify-content: space-between; align-items: start; gap: 10px; }
.ghost { border: 1px solid var(--bb-border); background: var(--bb-panel-hover); border-radius: 10px; padding: 6px 10px; cursor: pointer; color: var(--bb-text); }
.list { list-style: none; padding: 0; display: grid; grid-template-columns: 1fr; gap: 10px; }
@media (min-width: 900px) { .list { grid-template-columns: 1fr 1fr; } }
.card { border: 1px solid var(--bb-border); border-radius: 14px; padding: 12px; background: var(--bb-panel); }
.title {
color: var(--bb-text);
font-weight: 700;
text-decoration: none;
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.muted { color: var(--bb-muted); font-size: 12px; overflow-wrap: anywhere; margin-top: 6px; }
.error { color: #fecaca; }
.muted { color: var(--bb-muted); font-size: 12px; }
</style>