feat: 优化页面样式
This commit is contained in:
@@ -231,3 +231,25 @@ describe('BoardToolbar 多选', () => {
|
||||
expect(exactButton(wrapper, '多选')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('BoardToolbar 溢出容器', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
/**
|
||||
* 回归守卫:筛选组一旦变成滚动容器(overflow-x-auto 会让 overflow-y 也算 auto),
|
||||
* 「标签」下拉面板就会被裁在 36px 高的行里 —— 点开等于没反应,右侧还多一条滚动条。
|
||||
* 空间不够时应当换行(flex-wrap),而不是把内容推到滚动区外。
|
||||
*/
|
||||
it('筛选组不是滚动容器,标签面板不会被裁', () => {
|
||||
const { wrapper } = setup()
|
||||
const chip = buttonByText(wrapper, '今日到期')
|
||||
const group = chip?.element.parentElement
|
||||
|
||||
expect(group).toBeTruthy()
|
||||
const classes = group?.className ?? ''
|
||||
expect(classes).toContain('flex-wrap')
|
||||
expect(classes).not.toMatch(/overflow-(x-|y-)?(auto|scroll|hidden)/)
|
||||
})
|
||||
})
|
||||
|
||||
+177
-91
@@ -47,12 +47,17 @@ defineExpose({ focusSearch })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="flex flex-col gap-2.5">
|
||||
<header class="flex flex-col gap-2">
|
||||
<!--
|
||||
单行三段:搜索 │ 筛选 │ 操作。
|
||||
早先是两行(第一行筛选、第二行统计 + 批量操作),「今日到期 / 逾期」
|
||||
又被 ml-auto 顶到最右端,与搜索之间空出半个屏幕 —— 同一件事被切到两个空间。
|
||||
-->
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="relative min-w-56 flex-1 sm:max-w-xs">
|
||||
<div class="relative min-w-44 flex-1 max-sm:order-1 max-sm:min-w-0 sm:max-w-[260px] xl:max-w-[320px]">
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
class="pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-slate-400 dark:text-zinc-500"
|
||||
class="pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-slate-500 dark:text-zinc-400"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
@@ -63,136 +68,217 @@ defineExpose({ focusSearch })
|
||||
ref="searchRef"
|
||||
v-model="keyword"
|
||||
type="search"
|
||||
class="h-9 w-full rounded-md border border-slate-200 bg-white pr-2.5 pl-8 text-sm text-slate-700 outline-none transition placeholder:text-slate-400 focus:border-sky-400 focus:ring-2 focus:ring-sky-100 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200 dark:placeholder:text-zinc-500 dark:focus:border-sky-600 dark:focus:ring-sky-950"
|
||||
class="h-9 w-full rounded-lg border border-slate-200 bg-white pr-2.5 pl-8 text-sm text-slate-700 outline-none transition placeholder:text-slate-500 focus:border-sky-400 focus:ring-2 focus:ring-sky-100 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200 dark:placeholder:text-zinc-400 dark:focus:border-sky-600 dark:focus:ring-sky-950"
|
||||
placeholder="搜索标题或描述"
|
||||
aria-label="搜索任务"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex h-9 items-center gap-0.5 rounded-md border border-slate-200 bg-white p-0.5 dark:border-zinc-700 dark:bg-zinc-900"
|
||||
role="group"
|
||||
aria-label="按优先级筛选"
|
||||
>
|
||||
<button
|
||||
v-for="value in PRIORITY_ORDER"
|
||||
:key="value"
|
||||
type="button"
|
||||
class="flex h-full items-center rounded px-2.5 text-xs transition"
|
||||
:class="
|
||||
priorities.includes(value)
|
||||
? 'bg-slate-900 font-medium text-white dark:bg-zinc-100 dark:text-zinc-900'
|
||||
: 'text-slate-600 hover:bg-slate-100 dark:text-zinc-400 dark:hover:bg-zinc-800'
|
||||
"
|
||||
:aria-pressed="priorities.includes(value)"
|
||||
@click="filters.togglePriority(value)"
|
||||
>
|
||||
{{ PRIORITY_LABELS[value] }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<TagFilter
|
||||
:tags="store.allTags"
|
||||
:selected="tags"
|
||||
@toggle="filters.toggleTag($event)"
|
||||
@clear="filters.clearTags()"
|
||||
@manage="emit('manage-tags')"
|
||||
/>
|
||||
|
||||
<div class="ml-auto flex items-center gap-1.5">
|
||||
<!--
|
||||
筛选:快捷截止日期 + 优先级 + 标签,同一个语义组。
|
||||
这里**不能用 overflow-x-auto**:浏览器会把 overflow-y 一并算成 auto,
|
||||
于是这一条 36px 高的行变成滚动容器,标签下拉面板(~175px 高)被裁在盒外
|
||||
—— 表现就是「点了没反应」,右侧还多出一条滚动条。空间不够时改为换行。
|
||||
-->
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-1.5 max-sm:order-3 max-sm:w-full">
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-9 items-center gap-1.5 rounded-md border px-2.5 text-xs transition"
|
||||
class="flex h-9 shrink-0 items-center gap-1.5 rounded-lg border px-2.5 text-[13px] whitespace-nowrap transition"
|
||||
:class="
|
||||
due === 'today'
|
||||
? 'border-amber-300 bg-amber-50 text-amber-800 dark:border-amber-700 dark:bg-amber-950 dark:text-amber-300'
|
||||
? 'border-amber-300 bg-amber-50 font-medium text-amber-800 dark:border-amber-700 dark:bg-amber-950 dark:text-amber-300'
|
||||
: 'border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:bg-slate-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:border-zinc-600 dark:hover:bg-zinc-800'
|
||||
"
|
||||
:aria-pressed="due === 'today'"
|
||||
@click="filters.toggleDue('today')"
|
||||
>
|
||||
今日到期
|
||||
<span class="font-semibold">{{ dueTodayCount }}</span>
|
||||
<span
|
||||
class="font-semibold tabular-nums"
|
||||
:class="due === 'today' || dueTodayCount === 0 ? '' : 'text-amber-700 dark:text-amber-400'"
|
||||
>
|
||||
{{ dueTodayCount }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-9 items-center gap-1.5 rounded-md border px-2.5 text-xs transition"
|
||||
class="flex h-9 shrink-0 items-center gap-1.5 rounded-lg border px-2.5 text-[13px] whitespace-nowrap transition"
|
||||
:class="
|
||||
due === 'overdue'
|
||||
? 'border-rose-300 bg-rose-50 text-rose-700 dark:border-rose-700 dark:bg-rose-950 dark:text-rose-300'
|
||||
? 'border-rose-300 bg-rose-50 font-medium text-rose-700 dark:border-rose-700 dark:bg-rose-950 dark:text-rose-300'
|
||||
: overdueCount > 0
|
||||
? 'border-slate-200 bg-white text-rose-600 hover:border-rose-200 hover:bg-rose-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-rose-400 dark:hover:border-rose-800 dark:hover:bg-rose-950'
|
||||
? 'border-slate-200 bg-white text-slate-600 hover:border-rose-200 hover:bg-rose-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:border-rose-800 dark:hover:bg-rose-950'
|
||||
: 'border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:bg-slate-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:border-zinc-600 dark:hover:bg-zinc-800'
|
||||
"
|
||||
:aria-pressed="due === 'overdue'"
|
||||
@click="filters.toggleDue('overdue')"
|
||||
>
|
||||
逾期
|
||||
<span class="font-semibold">{{ overdueCount }}</span>
|
||||
<span
|
||||
class="font-semibold tabular-nums"
|
||||
:class="due === 'overdue' || overdueCount === 0 ? '' : 'text-rose-600 dark:text-rose-400'"
|
||||
>
|
||||
{{ overdueCount }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-x-2 gap-y-1.5 text-xs text-slate-500 dark:text-zinc-400">
|
||||
<span>共 {{ store.activeTasks.length }} 个任务</span>
|
||||
<span v-if="hiddenNote" class="text-slate-400 dark:text-zinc-500">{{ hiddenNote }}</span>
|
||||
<span class="hidden h-5 w-px shrink-0 bg-slate-200 sm:block dark:bg-zinc-700" aria-hidden="true" />
|
||||
|
||||
<div class="ml-auto flex items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-8 items-center rounded-md border border-slate-200 bg-white px-2.5 text-xs text-slate-600 transition hover:border-slate-300 hover:bg-slate-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:border-zinc-600 dark:hover:bg-zinc-800"
|
||||
@click="emit('batch-add')"
|
||||
<div
|
||||
class="flex h-9 shrink-0 items-center gap-0.5 rounded-lg border border-slate-200 bg-white p-0.5 dark:border-zinc-700 dark:bg-zinc-900"
|
||||
role="group"
|
||||
aria-label="按优先级筛选"
|
||||
>
|
||||
批量添加
|
||||
</button>
|
||||
<button
|
||||
v-for="value in PRIORITY_ORDER"
|
||||
:key="value"
|
||||
type="button"
|
||||
class="flex h-full items-center rounded-md px-2.5 text-[13px] whitespace-nowrap transition"
|
||||
:class="
|
||||
priorities.includes(value)
|
||||
? 'bg-sky-50 font-semibold text-sky-700 ring-1 ring-sky-200 dark:bg-sky-950 dark:text-sky-300 dark:ring-sky-800'
|
||||
: 'text-slate-600 hover:bg-slate-100 dark:text-zinc-400 dark:hover:bg-zinc-800'
|
||||
"
|
||||
:aria-pressed="priorities.includes(value)"
|
||||
@click="filters.togglePriority(value)"
|
||||
>
|
||||
{{ PRIORITY_LABELS[value] }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-8 items-center rounded-md border px-2.5 text-xs transition"
|
||||
:class="
|
||||
selecting
|
||||
? 'border-sky-300 bg-sky-50 text-sky-700 dark:border-sky-700 dark:bg-sky-950 dark:text-sky-300'
|
||||
: 'border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:bg-slate-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:border-zinc-600 dark:hover:bg-zinc-800'
|
||||
"
|
||||
:aria-pressed="selecting === true"
|
||||
@click="emit('toggle-select')"
|
||||
>
|
||||
{{ selecting ? '退出多选' : '多选' }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="archivableCount > 0"
|
||||
type="button"
|
||||
class="flex h-8 items-center rounded-md border border-slate-200 bg-white px-2.5 text-xs text-slate-600 transition hover:border-slate-300 hover:bg-slate-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:border-zinc-600 dark:hover:bg-zinc-800"
|
||||
@click="emit('archive')"
|
||||
>
|
||||
归档 {{ archivableCount }} 个已完成
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-8 items-center rounded-md border px-2.5 text-xs transition"
|
||||
:class="
|
||||
includeArchived
|
||||
? 'border-sky-300 bg-sky-50 text-sky-700 dark:border-sky-700 dark:bg-sky-950 dark:text-sky-300'
|
||||
: 'border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:bg-slate-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:border-zinc-600 dark:hover:bg-zinc-800'
|
||||
"
|
||||
:aria-pressed="includeArchived"
|
||||
@click="filters.toggleArchived()"
|
||||
>
|
||||
显示归档
|
||||
</button>
|
||||
<TagFilter
|
||||
:tags="store.allTags"
|
||||
:selected="tags"
|
||||
@toggle="filters.toggleTag($event)"
|
||||
@clear="filters.clearTags()"
|
||||
@manage="emit('manage-tags')"
|
||||
/>
|
||||
|
||||
<!-- 筛选生效时才出现:常态下工具栏不摆一个点了没事干的按钮 -->
|
||||
<button
|
||||
v-if="active"
|
||||
type="button"
|
||||
class="flex h-8 items-center rounded-md px-2.5 text-xs text-slate-500 underline-offset-2 transition hover:text-slate-800 hover:underline dark:text-zinc-400 dark:hover:text-zinc-200"
|
||||
class="flex h-9 shrink-0 items-center gap-1 rounded-lg px-2 text-[13px] whitespace-nowrap text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
|
||||
@click="filters.clear()"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
class="size-3.5 shrink-0"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="m5.5 5.5 9 9M14.5 5.5l-9 9" />
|
||||
</svg>
|
||||
清空筛选
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 操作:主操作(批量添加)+ 两个视图开关,全部平铺,不再收进菜单 -->
|
||||
<div class="ml-auto flex shrink-0 items-center gap-1.5 max-sm:order-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-9 items-center gap-1.5 rounded-lg bg-sky-700 px-3 text-[13px] font-medium whitespace-nowrap text-white transition hover:bg-sky-800 active:scale-[0.98] max-lg:w-9 max-lg:justify-center max-lg:px-0 dark:bg-sky-500 dark:text-slate-950 dark:hover:bg-sky-400"
|
||||
aria-label="批量添加"
|
||||
title="批量添加"
|
||||
@click="emit('batch-add')"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
class="size-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M3.5 5.5h9M3.5 10h9M3.5 14.5h5" />
|
||||
<path d="M15 11.5v5M12.5 14h5" />
|
||||
</svg>
|
||||
<span class="max-lg:hidden">批量添加</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-9 items-center gap-1.5 rounded-lg border px-2.5 text-[13px] whitespace-nowrap transition max-lg:w-9 max-lg:justify-center max-lg:px-0"
|
||||
:class="
|
||||
selecting
|
||||
? 'border-sky-300 bg-sky-50 font-medium text-sky-700 dark:border-sky-700 dark:bg-sky-950 dark:text-sky-300'
|
||||
: 'border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:bg-slate-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:border-zinc-600 dark:hover:bg-zinc-800'
|
||||
"
|
||||
:aria-pressed="selecting === true"
|
||||
:aria-label="selecting ? '退出多选' : '多选'"
|
||||
:title="selecting ? '退出多选' : '多选'"
|
||||
@click="emit('toggle-select')"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
class="size-4 shrink-0"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect x="3.5" y="3.5" width="13" height="13" rx="3" />
|
||||
<path d="m7.2 10.2 2 2 3.6-4.1" stroke-width="1.7" />
|
||||
</svg>
|
||||
<span class="max-lg:hidden">{{ selecting ? '退出多选' : '多选' }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-9 items-center gap-1.5 rounded-lg border px-2.5 text-[13px] whitespace-nowrap transition max-lg:w-9 max-lg:justify-center max-lg:px-0"
|
||||
:class="
|
||||
includeArchived
|
||||
? 'border-sky-300 bg-sky-50 font-medium text-sky-700 dark:border-sky-700 dark:bg-sky-950 dark:text-sky-300'
|
||||
: 'border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:bg-slate-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:border-zinc-600 dark:hover:bg-zinc-800'
|
||||
"
|
||||
:aria-pressed="includeArchived"
|
||||
aria-label="显示归档"
|
||||
title="显示归档"
|
||||
@click="filters.toggleArchived()"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
class="size-4 shrink-0"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect x="3" y="4.5" width="14" height="3.5" rx="1" />
|
||||
<path d="M4.6 8v6.4c0 .9.7 1.6 1.6 1.6h7.6c.9 0 1.6-.7 1.6-1.6V8" />
|
||||
<path d="M8.4 11h3.2" />
|
||||
</svg>
|
||||
<span class="max-lg:hidden">显示归档</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-slate-500 dark:text-zinc-400">
|
||||
<span>共 {{ store.activeTasks.length }} 个任务</span>
|
||||
<span v-if="hiddenNote" class="cursor-help" :title="`${hiddenNote};此时列内排序会暂停`">
|
||||
{{ hiddenNote }}
|
||||
</span>
|
||||
<!--
|
||||
归档是「有活了才出现」的清理动作,不该常驻占一个按钮位;
|
||||
挪到这一行后只有真的攒够可归档任务时才露头。
|
||||
-->
|
||||
<button
|
||||
v-if="archivableCount > 0"
|
||||
type="button"
|
||||
class="rounded text-slate-600 underline decoration-slate-300 underline-offset-2 transition hover:text-slate-900 hover:decoration-slate-500 dark:text-zinc-400 dark:decoration-zinc-600 dark:hover:text-zinc-200"
|
||||
@click="emit('archive')"
|
||||
>
|
||||
归档 {{ archivableCount }} 个已完成
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { nextTick } from 'vue'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import TagFilter from './TagFilter.vue'
|
||||
@@ -137,3 +138,35 @@ describe('TagFilter 空状态', () => {
|
||||
expect(wrapper.text()).toContain('还没有任何标签')
|
||||
})
|
||||
})
|
||||
|
||||
describe('TagFilter 面板定位', () => {
|
||||
it('靠近视口右缘时把面板往左推回视口内', async () => {
|
||||
const wrapper = mountFilter()
|
||||
const root = wrapper.element as HTMLElement
|
||||
// 模拟「标签」被挤到窗口右侧 60px 处:224px 的面板必然伸出窗口
|
||||
root.getBoundingClientRect = () =>
|
||||
({ left: window.innerWidth - 60, top: 0, width: 60, height: 36 }) as DOMRect
|
||||
|
||||
await triggerButton(wrapper).trigger('click')
|
||||
await nextTick()
|
||||
|
||||
const panel = wrapper.find('div.absolute').element as HTMLElement
|
||||
const shift = Number.parseFloat(panel.style.marginLeft || '0')
|
||||
expect(shift).toBeLessThan(0)
|
||||
// 右缘不能越过视口
|
||||
expect(window.innerWidth - 60 + shift + 224).toBeLessThanOrEqual(window.innerWidth)
|
||||
})
|
||||
|
||||
it('左侧空间充足时不偏移', async () => {
|
||||
const wrapper = mountFilter()
|
||||
const root = wrapper.element as HTMLElement
|
||||
root.getBoundingClientRect = () =>
|
||||
({ left: 100, top: 0, width: 60, height: 36 }) as DOMRect
|
||||
|
||||
await triggerButton(wrapper).trigger('click')
|
||||
await nextTick()
|
||||
|
||||
const panel = wrapper.find('div.absolute').element as HTMLElement
|
||||
expect(Number.parseFloat(panel.style.marginLeft || '0')).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* 改成下拉面板:平时只占一个按钮,选中的数量用角标提示。
|
||||
*/
|
||||
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
defineProps<{ tags: readonly string[]; selected: readonly string[] }>()
|
||||
|
||||
@@ -15,6 +15,34 @@ const emit = defineEmits<{ toggle: [tag: string]; clear: []; manage: [] }>()
|
||||
const open = ref(false)
|
||||
const rootRef = ref<HTMLElement | null>(null)
|
||||
|
||||
/**
|
||||
* 面板固定 224px(w-56)且默认左对齐触发按钮。
|
||||
* 当「标签」被挤到靠近视口右缘时(窄屏工具栏、筛选换行的情况),
|
||||
* 面板会伸出窗口被裁掉 —— 这里把面板往左推回视口内。
|
||||
* 用 margin-left 而不是 transform:面板自身的 pop-in 动画也在动 transform,两者会打架。
|
||||
*/
|
||||
const PANEL_WIDTH = 224
|
||||
const EDGE = 8
|
||||
const shiftX = ref(0)
|
||||
|
||||
function reposition(): void {
|
||||
const root = rootRef.value
|
||||
if (!root) return
|
||||
const rect = root.getBoundingClientRect()
|
||||
const overflowRight = rect.left + PANEL_WIDTH - (window.innerWidth - EDGE)
|
||||
shiftX.value = overflowRight > 0 ? -Math.min(overflowRight, Math.max(rect.left - EDGE, 0)) : 0
|
||||
}
|
||||
|
||||
function onResize(): void {
|
||||
if (open.value) reposition()
|
||||
}
|
||||
|
||||
watch(open, async (value) => {
|
||||
if (!value) return
|
||||
await nextTick()
|
||||
reposition()
|
||||
})
|
||||
|
||||
function onDocumentClick(event: MouseEvent): void {
|
||||
if (!open.value) return
|
||||
const root = rootRef.value
|
||||
@@ -38,11 +66,13 @@ function openManager(): void {
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', onDocumentClick)
|
||||
document.addEventListener('keydown', onDocumentKeydown, true)
|
||||
window.addEventListener('resize', onResize)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', onDocumentClick)
|
||||
document.removeEventListener('keydown', onDocumentKeydown, true)
|
||||
window.removeEventListener('resize', onResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -50,10 +80,10 @@ onBeforeUnmount(() => {
|
||||
<div ref="rootRef" class="relative">
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-9 items-center gap-1.5 rounded-md border px-2.5 text-xs transition"
|
||||
class="flex h-9 shrink-0 items-center gap-1.5 rounded-lg border px-2.5 text-[13px] whitespace-nowrap transition"
|
||||
:class="
|
||||
selected.length > 0
|
||||
? 'border-sky-300 bg-sky-50 text-sky-700 dark:border-sky-700 dark:bg-sky-950 dark:text-sky-300'
|
||||
? 'border-sky-300 bg-sky-50 font-medium text-sky-700 dark:border-sky-700 dark:bg-sky-950 dark:text-sky-300'
|
||||
: 'border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:bg-slate-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:border-zinc-600 dark:hover:bg-zinc-800'
|
||||
"
|
||||
:aria-expanded="open"
|
||||
@@ -63,7 +93,7 @@ onBeforeUnmount(() => {
|
||||
标签
|
||||
<span
|
||||
v-if="selected.length > 0"
|
||||
class="rounded-full bg-sky-600 px-1.5 text-[11px] leading-4 font-medium text-white dark:bg-sky-500"
|
||||
class="rounded-full bg-sky-700 px-1.5 text-[11px] leading-4 font-medium text-white tabular-nums dark:bg-sky-400 dark:text-sky-950"
|
||||
>
|
||||
{{ selected.length }}
|
||||
</span>
|
||||
@@ -87,8 +117,9 @@ onBeforeUnmount(() => {
|
||||
<div
|
||||
v-if="open"
|
||||
class="absolute top-full left-0 z-20 mt-1.5 w-56 animate-pop-in rounded-lg border border-slate-200 bg-white p-1.5 shadow-pop shadow-pop-dark motion-reduce:animate-none dark:border-zinc-700 dark:bg-zinc-900"
|
||||
:style="{ marginLeft: `${shiftX}px` }"
|
||||
>
|
||||
<p v-if="tags.length === 0" class="px-2 py-3 text-center text-xs text-slate-400 dark:text-zinc-500">
|
||||
<p v-if="tags.length === 0" class="px-2 py-3 text-center text-xs text-slate-500 dark:text-zinc-400">
|
||||
还没有任何标签
|
||||
</p>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user