feat: 实现文件夹和书签的持久排序与拖拽功能

This commit is contained in:
2026-01-18 23:33:31 +08:00
parent 6eb3c730bb
commit dbeb181e5d
49 changed files with 3141 additions and 507 deletions

View File

@@ -2,7 +2,7 @@
import { ref } from "vue";
import { useRouter } from "vue-router";
import { apiFetch } from "../../lib/api";
import { getToken, setToken } from "../../lib/extStorage";
import { setToken } from "../../lib/extStorage";
import { clearLocalState, mergeLocalToUser } from "../../lib/localData";
const router = useRouter();
@@ -35,20 +35,13 @@ async function submit() {
// After merge, keep extension in cloud mode
await clearLocalState();
await router.push("/my");
await router.replace("/");
} catch (e) {
error.value = e.message || String(e);
} finally {
loading.value = false;
}
}
async function logout() {
const token = await getToken();
if (!token) return;
await setToken("");
await router.push("/");
}
</script>
<template>
@@ -58,7 +51,6 @@ async function logout() {
<div class="row">
<button class="tab" :class="{ active: mode === 'login' }" @click="mode = 'login'">登录</button>
<button class="tab" :class="{ active: mode === 'register' }" @click="mode = 'register'">注册</button>
<button class="tab" @click="logout">退出</button>
</div>
<div class="form">

View File

@@ -0,0 +1,74 @@
<script setup>
import { computed, onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { getToken, setToken } from "../../lib/extStorage";
const router = useRouter();
const token = ref("");
const loggedIn = computed(() => Boolean(token.value));
const webBaseUrl = import.meta.env.VITE_WEB_BASE_URL || "http://localhost:5173";
async function refresh() {
token.value = await getToken();
}
function openWeb() {
const url = String(webBaseUrl || "").trim();
if (!url) return;
if (typeof chrome !== "undefined" && chrome.tabs?.create) chrome.tabs.create({ url });
else window.open(url, "_blank", "noopener,noreferrer");
}
async function logout() {
await setToken("");
await refresh();
await router.replace("/login");
}
onMounted(refresh);
</script>
<template>
<section class="page">
<h1 class="h1">更多操作</h1>
<p v-if="!loggedIn" class="muted">当前未登录将跳转到登录页</p>
<div class="card">
<button class="btn" type="button" @click="openWeb">跳转 Web</button>
<button class="btn btn--secondary" type="button" @click="logout">退出登录</button>
<p class="hint">Web 地址来自环境变量VITE_WEB_BASE_URL</p>
</div>
</section>
</template>
<style scoped>
.page { padding: 14px; }
.h1 { margin: 0 0 10px; font-size: 18px; }
.card {
max-width: 560px;
border: 1px solid rgba(255,255,255,0.65);
background: var(--bb-card);
border-radius: 18px;
padding: 12px;
display: grid;
gap: 10px;
}
.btn {
border: 1px solid rgba(255,255,255,0.25);
border-radius: 14px;
padding: 10px 12px;
background: linear-gradient(135deg, var(--bb-primary), var(--bb-cta));
color: white;
cursor: pointer;
}
.btn--secondary {
background: rgba(255,255,255,0.55);
color: var(--bb-text);
border-color: var(--bb-border);
}
.muted { color: rgba(19, 78, 74, 0.72); font-size: 12px; }
.hint { color: rgba(19, 78, 74, 0.72); font-size: 12px; margin: 0; }
</style>

View File

@@ -44,6 +44,7 @@ async function add() {
async function remove(id) {
if (mode.value !== "local") return;
if (!confirm("确定删除该书签?")) return;
await markLocalDeleted(id);
await load();
}
@@ -86,7 +87,16 @@ onMounted(load);
.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 #e5e7eb; border-radius: 14px; padding: 12px; background: white; }
.title { color: #111827; font-weight: 700; text-decoration: none; }
.title {
color: #111827;
font-weight: 700;
text-decoration: none;
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.muted { color: #475569; font-size: 12px; overflow-wrap: anywhere; margin-top: 6px; }
.error { color: #b91c1c; }
.muted { color: #475569; font-size: 12px; }

View File

@@ -47,7 +47,15 @@ onMounted(load);
.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 #e5e7eb; border-radius: 14px; padding: 12px; background: white; }
.title { color: #111827; font-weight: 700; text-decoration: none; }
.title {
color: #111827;
font-weight: 700;
text-decoration: none;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.muted { color: #475569; font-size: 12px; overflow-wrap: anywhere; margin-top: 6px; }
.error { color: #b91c1c; }
</style>