This repository has been archived on 2026-04-20. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
chrom-tool/src/components/browser-data/BookmarksList.vue
Julian Freeman c407a78991 fix ui
2026-04-18 11:56:57 -04:00

349 lines
8.1 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 lang="ts">
import { computed } from "vue";
import type { BookmarkSortKey, BookmarkSummary } from "../../types/browser";
const props = defineProps<{
bookmarks: BookmarkSummary[];
sortKey: BookmarkSortKey;
selectedBookmarkUrls: string[];
deleteBusy: boolean;
}>();
const emit = defineEmits<{
"update:sortKey": [value: BookmarkSortKey];
showProfiles: [url: string];
toggleBookmark: [url: string];
toggleAllBookmarks: [];
deleteBookmark: [url: string];
deleteSelected: [];
}>();
const allSelected = computed(
() =>
props.bookmarks.length > 0 &&
props.bookmarks.every((bookmark) => props.selectedBookmarkUrls.includes(bookmark.url)),
);
function isSelected(url: string) {
return props.selectedBookmarkUrls.includes(url);
}
</script>
<template>
<section class="table-section">
<div v-if="bookmarks.length" class="data-table">
<div class="bookmarks-toolbar">
<label class="toolbar-checkbox" :class="{ disabled: !bookmarks.length }">
<input
type="checkbox"
class="native-checkbox"
:checked="allSelected"
:disabled="!bookmarks.length || deleteBusy"
@change="emit('toggleAllBookmarks')"
/>
<span class="custom-checkbox" :class="{ checked: allSelected }" aria-hidden="true">
<svg viewBox="0 0 16 16">
<path d="M3.5 8.2L6.4 11.1L12.5 4.9" />
</svg>
</span>
<span>全选</span>
</label>
<button
class="danger-button toolbar-danger-button"
type="button"
:disabled="!selectedBookmarkUrls.length || deleteBusy"
@click="emit('deleteSelected')"
>
{{ deleteBusy ? "删除中..." : `删除所选(${selectedBookmarkUrls.length}` }}
</button>
</div>
<div class="data-table-header bookmarks-grid">
<div class="header-cell checkbox-cell">选择</div>
<button class="header-cell sortable" :class="{ active: sortKey === 'title' }" type="button" @click="emit('update:sortKey', 'title')">名称</button>
<button class="header-cell sortable" :class="{ active: sortKey === 'url' }" type="button" @click="emit('update:sortKey', 'url')">URL</button>
<div class="header-cell actions-cell">操作</div>
</div>
<div class="data-table-body styled-scrollbar">
<article v-for="bookmark in bookmarks" :key="bookmark.url" class="data-table-row bookmarks-grid">
<div class="row-cell checkbox-cell">
<label class="table-checkbox" :class="{ disabled: deleteBusy }">
<input
type="checkbox"
class="native-checkbox"
:checked="isSelected(bookmark.url)"
:disabled="deleteBusy"
@change="emit('toggleBookmark', bookmark.url)"
/>
<span class="custom-checkbox" :class="{ checked: isSelected(bookmark.url) }" aria-hidden="true">
<svg viewBox="0 0 16 16">
<path d="M3.5 8.2L6.4 11.1L12.5 4.9" />
</svg>
</span>
</label>
</div>
<div class="row-cell primary-cell">
<strong :title="bookmark.title">{{ bookmark.title }}</strong>
</div>
<div class="row-cell muted-cell" :title="bookmark.url">{{ bookmark.url }}</div>
<div class="row-cell actions-cell">
<button class="disclosure-button" type="button" @click="emit('showProfiles', bookmark.url)">
<span>查看</span>
<span class="badge neutral">{{ bookmark.profileIds.length }}</span>
</button>
<button
class="danger-button inline-danger-button"
type="button"
:disabled="deleteBusy"
@click="emit('deleteBookmark', bookmark.url)"
>
删除
</button>
</div>
</article>
</div>
</div>
<div v-else class="empty-card">
<p>这个浏览器没有扫描到任何书签</p>
</div>
</section>
</template>
<style scoped>
.table-section {
padding: 0;
height: 100%;
min-height: 0;
}
.bookmarks-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 12px 8px;
border-bottom: 1px solid rgba(148, 163, 184, 0.1);
}
.data-table {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
border: 1px solid rgba(148, 163, 184, 0.18);
border-radius: 22px;
background: var(--panel);
box-shadow: var(--shadow);
overflow: hidden;
}
.data-table-body {
min-height: 0;
overflow: auto;
scrollbar-gutter: stable;
}
.bookmarks-grid {
display: grid;
grid-template-columns: 52px minmax(180px, 0.9fr) minmax(260px, 1.2fr) 250px;
gap: 12px;
align-items: center;
}
.data-table-header {
position: sticky;
top: 0;
z-index: 2;
padding: 8px 24px 8px 12px;
border-bottom: 1px solid rgba(148, 163, 184, 0.14);
background: rgba(248, 250, 252, 0.94);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.header-cell {
color: var(--muted);
font-size: 0.81rem;
font-weight: 700;
letter-spacing: 0.02em;
}
.header-cell.sortable {
padding: 0;
text-align: left;
background: transparent;
cursor: pointer;
}
.header-cell.sortable.active {
color: var(--text);
}
.toolbar-checkbox {
display: inline-flex;
align-items: center;
gap: 10px;
color: var(--text);
font-size: 0.88rem;
cursor: pointer;
}
.toolbar-checkbox.disabled {
opacity: 0.55;
cursor: default;
}
.native-checkbox {
position: absolute;
opacity: 0;
pointer-events: none;
}
.table-checkbox {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.table-checkbox.disabled {
cursor: default;
opacity: 0.5;
}
.custom-checkbox {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border: 1px solid rgba(148, 163, 184, 0.34);
border-radius: 7px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(241, 245, 249, 0.92));
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.78),
0 4px 10px rgba(15, 23, 42, 0.06);
}
.custom-checkbox svg {
width: 12px;
height: 12px;
}
.custom-checkbox path {
fill: none;
stroke: #fff;
stroke-width: 2.2;
stroke-linecap: round;
stroke-linejoin: round;
opacity: 0;
}
.custom-checkbox.checked {
border-color: rgba(47, 111, 237, 0.2);
background: linear-gradient(135deg, #2f6fed, #5aa1f7);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.22),
0 8px 18px rgba(47, 111, 237, 0.22);
}
.custom-checkbox.checked path {
opacity: 1;
}
.data-table-row {
padding: 10px 12px;
border-bottom: 1px solid rgba(148, 163, 184, 0.12);
}
.data-table-row:last-child {
border-bottom: 0;
}
.data-table-row:hover {
background: rgba(248, 250, 252, 0.65);
}
.row-cell {
min-width: 0;
}
.primary-cell strong {
display: block;
font-size: 0.93rem;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.muted-cell {
color: var(--muted);
font-size: 0.87rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.disclosure-button {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
width: fit-content;
min-width: 120px;
padding: 5px 10px;
border-radius: 10px;
background: rgba(241, 245, 249, 0.9);
color: var(--badge-text);
font-size: 0.8rem;
cursor: pointer;
}
.disclosure-button .badge {
min-width: 24px;
padding: 4px 8px;
font-size: 0.76rem;
}
.actions-cell {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.checkbox-cell {
display: flex;
justify-content: center;
}
.inline-danger-button {
padding: 5px 10px;
border-radius: 10px;
font-size: 0.8rem;
}
.toolbar-danger-button {
padding: 6px 10px;
border-radius: 10px;
}
@media (max-width: 900px) {
.bookmarks-grid {
grid-template-columns: 52px minmax(160px, 0.9fr) minmax(200px, 1fr) 236px;
}
}
@media (max-width: 720px) {
.bookmarks-grid {
grid-template-columns: 52px minmax(0, 1fr) 152px;
}
.bookmarks-grid > :nth-child(3) {
display: none;
}
}
</style>