﻿<template>
  <div
    class="relative overflow-hidden bg-[radial-gradient(circle_at_12%_18%,rgba(215,177,123,0.24),transparent_24%),radial-gradient(circle_at_86%_12%,rgba(120,162,182,0.18),transparent_24%),radial-gradient(circle_at_50%_100%,rgba(176,198,172,0.14),transparent_28%),linear-gradient(180deg,#efe7da_0%,#e8f0ef_44%,#f3f1e8_100%)]"
    :style="contentStyle"
  >
    <div class="pointer-events-none absolute left-[-5rem] top-6 h-80 w-80 rounded-full bg-[rgba(234,176,107,0.45)] opacity-48 blur-[88px]"></div>
    <div class="pointer-events-none absolute right-[-5rem] top-32 h-[22rem] w-[22rem] rounded-full bg-[rgba(118,176,214,0.32)] opacity-48 blur-[88px]"></div>
    <div class="pointer-events-none absolute inset-y-0 left-5 right-5 opacity-14 [background-image:linear-gradient(rgba(255,255,255,0.18)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.18)_1px,transparent_1px)] [background-position:18px_0,18px_0] [background-size:36px_36px] [mask-image:linear-gradient(180deg,rgba(0,0,0,0.8),transparent_85%)]"></div>
    <n-layout has-sider>
      <n-layout-content class="relative !bg-transparent">
        <div ref="searchRef" class="relative z-[1] hidden px-12 pt-8 lg:block">
          <div ref="searchShellRef" class="search-shell relative">
            <n-input-group class="glass-surface overflow-hidden rounded-[22px] transition-[box-shadow,border-color] duration-300">
              <n-select
                class="w-24 search-select rounded-full"
                size="large"
                v-model:value="broswer"
                :options="options"
                :menu-props="{ class: 'search-select-menu' }"
                @click="cancelSbox"
              />
              <n-input
                ref="searchInputRef"
                class="flex-1"
                size="large"
                autofocus
                clearable
                v-model:value="searchWord"
                @blur="cancelSbox"
                placeholder="输入关键词，按 / 快速聚焦"
              >
                <template #suffix>
                  <n-icon class=" cursor-pointer" size="large">
                    <icon-search />
                  </n-icon>
                </template>
              </n-input>
            </n-input-group>
          </div>
        </div>

        <Teleport to="body">
          <div v-if="searchBox" class="search-suggest-portal pointer-events-none fixed inset-0 z-[1200]">
            <div
              class="search-suggest-panel glass-surface glass-floating-surface pointer-events-auto absolute rounded-[26px] px-3 py-3 text-sm text-slate-500"
              :style="searchPanelStyle"
            >
              <div
                v-for="(item, idx) in searchItems"
                :key="idx"
                class="search-suggest-item relative z-[1] flex items-center truncate rounded-[18px] border border-transparent p-3.5 pr-6 text-slate-500 transition-[transform,background-color,color,box-shadow,border-color] duration-200"
                :class="selecedIdx === idx
                  ? 'bg-[linear-gradient(135deg,#ec66ab,#f48a6f)] text-white shadow-[0_4px_10px_rgba(236,102,171,0.16),0_14px_28px_rgba(236,102,171,0.2)]'
                  : 'hover:-translate-y-0.5 hover:border-white/55 hover:bg-white/62 hover:text-slate-700 hover:shadow-[0_2px_6px_rgba(97,121,146,0.06),0_12px_24px_rgba(97,121,146,0.1)]'"
                :style="{ marginTop: idx === 0 ? '0' : '0.45rem' }"
                @click="goExtra(item.menu_link)"
                @keyup.enter="goExtra(item.menu_link)"
              >
                <span
                  class="mr-2 inline-flex h-6 shrink-0 items-center rounded-full px-2.5 text-[11px] font-semibold tracking-[0.04em]"
                  :class="selecedIdx === idx
                    ? 'bg-white/18 text-white'
                    : idx ? 'bg-slate-700/8 text-slate-500' : 'bg-[rgba(236,102,171,0.12)] text-[rgb(212,88,154)]'"
                >
                  {{ idx ? '导航' : '搜索' }}
                </span>
                <span class="truncate">{{ item.menu_name }}</span>
              </div>
              <div v-if="searchItems.length === 1" class="search-empty py-3 text-center text-[13px] text-slate-400">
                未找到匹配的导航，回车可直接搜索
              </div>
              <div class="search-hints mt-1.5 flex items-center justify-center gap-4 border-t border-white/55 pt-2.5 text-[11px] text-slate-400">
                <span><b class="kbd">↑</b><b class="kbd">↓</b>&nbsp;选择</span>
                <span><b class="kbd">↵</b>&nbsp;打开</span>
                <span><b class="kbd">Esc</b>&nbsp;关闭</span>
              </div>
            </div>
          </div>
        </Teleport>

        <n-scrollbar class="w-full overflow-x-auto" :style="navStyle" trigger="none">
          <div
            v-if="navlist.length"
            class="glass-surface mt-6 mb-4 flex flex-wrap items-center gap-1.5 rounded-[20px] px-2.5 py-2.5 lg:mx-12 lg:px-4"
          >
            <d-tag
              v-for="tag in tagList"
              :key="tag.name"
              class="nav-tag cursor-pointer truncate rounded-full bg-white/90 px-2 py-0.5 text-[12px] font-semibold text-slate-600 shadow-sm"
              :class="tag.checked ? 'text-white shadow-[0_4px_10px_rgba(15,23,42,0.12),0_10px_20px_rgba(15,23,42,0.08)]' : 'bg-white/90'"
              :style="tag.checked ? { backgroundColor: tag.color || '#334155' } : undefined"
              hideBeyondTags
              :checked="tag.checked"
              :color="tag.color"
              @click="handdleTagClick(tag)"
            >
              <span class="inline-flex items-center gap-1">
                <span
                  class="inline-block h-1 w-1 rounded-full ring-1 ring-white/70"
                  :style="{ backgroundColor: tag.color || '#94a3b8' }"
                ></span>
                <span>{{ tag.name }}</span>
                <span
                  class="inline-flex h-4 min-w-4 items-center justify-center rounded-full px-0.5 text-[9px] font-bold leading-none"
                  :class="tag.checked ? 'bg-white/25 text-white' : 'bg-slate-700/8 text-slate-400'"
                >{{ tag.count ?? 0 }}</span>
              </span>
            </d-tag>
          </div>

          <TransitionGroup
            ref="navcards"
            name="nav-card"
            tag="div"
            class="grid content-start grid-cols-2 gap-5 px-3 pb-8 pt-3 lg:grid-cols-3 lg:px-12 xl:grid-cols-4 2xl:grid-cols-6"
          >
            <div
              v-for="(item, index) in navlist"
              :key="item.nid ?? index"
              class="nav-card-wrap"
              :data-nid="item.nid"
              :style="{ '--i': index % 12 }"
            >
            <n-card
              embedded
              class="glass-nav-card h-28 box-border overflow-hidden rounded-[24px] transition-[transform,box-shadow,border-color] duration-[320ms] [transition-timing-function:cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-1"
              @click="goExtra(item.menu_link)"
              @contextmenu.prevent="handdleContextMenu($event, item)"
            >
              <div class="relative mt-1 w-full flex flex-col items-center cursor-pointer group">
                <icon-star
                  v-if="isPinned(item)"
                  class="nav-pin absolute -top-0.5 right-0 w-4 text-[#f0a13c] drop-shadow-[0_2px_6px_rgba(240,161,60,0.45)]"
                />
                <div
                  v-if="item.icon_error"
                  :style="{ background: getItemColor(item) }"
                  class="grid-image flex h-10 w-10 items-center justify-center rounded-2xl border border-white/95 bg-[linear-gradient(180deg,rgba(255,255,255,0.96),rgba(234,242,249,0.92))] p-[0.2rem] text-white shadow-[0_2px_6px_rgba(97,121,146,0.06),0_10px_20px_rgba(97,121,146,0.1),inset_0_1px_0_rgba(255,255,255,0.95)] transition-transform duration-[350ms] [transition-timing-function:cubic-bezier(0.22,1,0.36,1)]"
                >
                  {{ item.first }}
                </div>
                <img
                  v-else
                  class="grid-image h-10 w-10 rounded-2xl border border-white/95 bg-[linear-gradient(180deg,rgba(255,255,255,0.96),rgba(234,242,249,0.92))] p-[0.2rem] object-cover shadow-[0_2px_6px_rgba(97,121,146,0.06),0_10px_20px_rgba(97,121,146,0.1),inset_0_1px_0_rgba(255,255,255,0.95)] transition-transform duration-[350ms] [transition-timing-function:cubic-bezier(0.22,1,0.36,1)]"
                  :key="item.menu_icon"
                  :src="item.menu_icon"
                  :alt="item.menu_name"
                  @error="imgErr(index)"
                />
                <div class="card-name mt-3 w-full truncate text-center text-lg !font-semibold text-slate-700">{{ item.menu_name || '' }}</div>
              </div>
            </n-card>
            </div>

            <div
              v-if="addCardReady"
              key="add-card"
              class="nav-card-wrap"
              :style="{ '--i': Math.min(navlist.length, 11) }"
            >
            <n-card
              embedded
              class="glass-nav-card glass-nav-card--warm h-28 box-border overflow-hidden rounded-[24px] transition-[transform,box-shadow,border-color] duration-[320ms] [transition-timing-function:cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-1"
            >
              <div @click="addNav" class="w-full h-full flex flex-col items-center justify-center cursor-pointer">
                <div
                  :style="{ background: addCardColor }"
                  class="add-card-plus flex h-10 w-10 items-center justify-center rounded-2xl border border-white/95 bg-[linear-gradient(180deg,rgba(255,255,255,0.96),rgba(234,242,249,0.92))] p-[0.2rem] text-2xl text-white shadow-[0_2px_6px_rgba(97,121,146,0.06),0_10px_20px_rgba(97,121,146,0.1),inset_0_1px_0_rgba(255,255,255,0.95)] transition-transform duration-300 [transition-timing-function:cubic-bezier(0.22,1,0.36,1)]"
                >
                  +
                </div>
                <div class="mt-3 w-full truncate text-center text-base font-semibold text-primary">新增导航</div>
              </div>
            </n-card>
            </div>
          </TransitionGroup>
        </n-scrollbar>
      </n-layout-content>

      <n-layout-sider width="30rem">
        <homeSide />
      </n-layout-sider>
    </n-layout>

    <maskX :visible="visible" :setVisible="navCancel">
      <n-form
        class="w-[500px] rounded-[28px] border border-white/80 bg-[radial-gradient(circle_at_top_left,rgba(255,199,132,0.16),transparent_34%),radial-gradient(circle_at_top_right,rgba(136,203,255,0.14),transparent_28%),linear-gradient(180deg,rgba(255,255,255,0.84),rgba(242,247,251,0.78))] p-8 shadow-[0_4px_12px_rgba(75,96,120,0.08),0_20px_44px_rgba(75,96,120,0.12),0_64px_120px_rgba(178,128,88,0.08)] backdrop-blur-[20px]"
        ref="formNav"
        layout="vertical"
        :data="navData"
      >
        <div class="flex items-center justify-center gap-2 text-center text-xl font-bold tracking-[0.02em] text-slate-800">
          <icon-star class="w-5 text-[#f0a13c]" />
          新增导航
          <icon-star class="w-5 text-[#f0a13c]" />
        </div>
        <n-form-item class="h-8 modal-form-item" path="menu_link">
          <n-input class="modal-input" @blur="getIcon" v-model:value="navData.menu_link" placeholder="请输入链接（必填）" />
        </n-form-item>
        <n-form-item class="h-8 mt-4 modal-form-item" path="menu_name">
          <n-input class="modal-input" v-model:value="navData.menu_name" placeholder="请输入导航名称（必填）" />
        </n-form-item>
        <n-form-item class="h-8 mt-4 modal-form-item" path="tag">
          <n-input class="modal-input" v-model:value="navData.tag" placeholder="请输入标签（最多4个字）" />
        </n-form-item>
        <div v-if="quickTagOptions.length" class="mt-2 flex flex-wrap items-center gap-1.5 pl-1">
          <span class="text-xs text-slate-400">常用：</span>
          <span
            v-for="t in quickTagOptions"
            :key="t"
            class="quick-tag"
            :class="navData.tag === t ? 'quick-tag--active' : ''"
            @click="navData.tag = t"
          >{{ t }}</span>
        </div>
        <n-form-item class="h-8 mt-4 modal-form-item" path="menu_icon">
          <n-input class="modal-input" v-model:value="navData.menu_icon" placeholder="请输入图标链接" />
          <img
            class="ml-5 rounded-[10px] bg-white/80 shadow-[0_8px_18px_rgba(92,118,144,0.12)]"
            v-if="navData.menu_icon"
            width="30"
            height="30"
            :src="navData.menu_icon"
            alt="icon"
          />
          <div v-else class="ml-5 w-[30px] h-[30px]"></div>
        </n-form-item>
        <div class="mt-14 flex justify-between">
          <n-button class="w-[48%] !h-11 !rounded-full !border-none !bg-white/90 !font-bold !tracking-[0.04em] !text-slate-600 shadow-[0_3px_8px_rgba(89,108,130,0.07),0_12px_24px_rgba(89,108,130,0.09)]" secondary variant="solid" @click="navCancel">取消</n-button>
          <n-button class="w-[48%] !h-11 !rounded-full !border-none !bg-[linear-gradient(135deg,#ec66ab,#f48a6f)] !font-bold !tracking-[0.04em] shadow-[0_4px_10px_rgba(236,102,171,0.18),0_14px_28px_rgba(236,102,171,0.2)]" type="primary" variant="solid" @click="navSubmit">确定</n-button>
        </div>
      </n-form>
    </maskX>

    <aplayer />

    <contextMenu :show="menuShow" :options="MenuOptions">
      <div class="menu-pop glass-floating-surface !m-0 cursor-pointer !rounded-2xl !p-0" @mouseenter="removeTimer" @mouseleave="hideMenu">
        <div class="menu-head truncate">{{ currentItem?.menu_name || '导航操作' }}</div>
        <div class="menu-item" @click="handdleMenuItem(1)">
          <icon-pen class="menu-icon" />修改名称
        </div>
        <div class="menu-item" @click="handdleMenuItem(2)">
          <icon-loc class="menu-icon" />修改链接
        </div>
        <div class="menu-item" @click="handdleMenuItem(3)">
          <icon-tag class="menu-icon" />修改标签
        </div>
        <div class="menu-divider"></div>
        <div class="menu-item" @click="handdleMenuItem(5)">
          <icon-download class="menu-icon" />复制链接
        </div>
        <div class="menu-item" @click="handdleMenuItem(6)">
          <icon-star class="menu-icon" :class="isPinned(currentItem) ? '!text-[#f0a13c]' : ''" />{{ isPinned(currentItem) ? '取消置顶' : '置顶导航' }}
        </div>
        <div class="menu-item" @click="handdleMenuItem(7)">
          <icon-loading class="menu-icon" />重新加载图标
        </div>
        <div class="menu-divider"></div>
        <div class="menu-item menu-item--danger" @click="handdleMenuItem(4)">
          删除导航
        </div>
      </div>
    </contextMenu>

    <maskX :visible="editModal" :setVisible="handleItemCancel">
      <div class="w-[500px] rounded-[28px] border border-white/80 bg-[radial-gradient(circle_at_top_left,rgba(255,199,132,0.16),transparent_34%),radial-gradient(circle_at_top_right,rgba(136,203,255,0.14),transparent_28%),linear-gradient(180deg,rgba(255,255,255,0.84),rgba(242,247,251,0.78))] p-8 shadow-[0_4px_12px_rgba(75,96,120,0.08),0_20px_44px_rgba(75,96,120,0.12),0_64px_120px_rgba(178,128,88,0.08)] backdrop-blur-[20px]">
        <div class="mb-8 flex items-center justify-center gap-2 text-center text-xl font-bold tracking-[0.02em] text-slate-800">
          <icon-pen class="w-5 text-primary" />
          修改导航内容
        </div>
        <div class="mb-4 text-slate-500">
          原内容：
          <span class="text-primary" v-if="currentClickedItem === 1">导航名称：{{ currentItem?.menu_name }}</span>
          <span class="text-primary" v-else-if="currentClickedItem === 2">导航链接：{{ currentItem?.menu_link }}</span>
          <span class="text-primary" v-else-if="currentClickedItem === 3">导航标签：{{ currentItem?.tag }}</span>
        </div>
        <n-input class="modal-input" v-model:value="editInput" placeholder="请输入修改内容" @keyup.enter="handleItemSubmit" />
        <div class="mt-8 flex justify-between">
          <n-button class="w-[48%] !h-11 !rounded-full !border-none !bg-white/90 !font-bold !tracking-[0.04em] !text-slate-600 shadow-[0_3px_8px_rgba(89,108,130,0.07),0_12px_24px_rgba(89,108,130,0.09)]" secondary @click="handleItemCancel">取消</n-button>
          <n-button class="w-[48%] !h-11 !rounded-full !border-none !bg-[linear-gradient(135deg,#ec66ab,#f48a6f)] !font-bold !tracking-[0.04em] shadow-[0_4px_10px_rgba(236,102,171,0.18),0_14px_28px_rgba(236,102,171,0.2)]" type="primary" @click="handleItemSubmit">确定</n-button>
        </div>
      </div>
    </maskX>
  </div>
</template>

<script setup lang="ts">
import contextMenu from '@/components/contextMenu.vue'
import maskX from '@/components/mask.vue'
import { deepclone } from '@/util/index.ts'

definePage({
  name: 'home',
  alias: '/home',
  meta: {
    title: '首页',
  }
})

interface NavItem {
  nid?: number
  menu_link: string
  menu_name: string
  tag: string
  menu_icon: string
  icon_error?: boolean
  first?: string
  color?: string
}

interface TagItem {
  name: string
  color: string
  checked: boolean
  count: number
}

interface SearchItem {
  menu_name: string
  menu_link: string
  tag: string
}

interface SearchEngineOption {
  label: string
  value: string
  url: string
}

/** localStorage 键名 */
const HITS_KEY = 'nav_hits'
const PINNED_KEY = 'nav_pinned'
const ENGINE_KEY = 'nav_search_engine'

const menuShow = ref(false)
const MenuOptions = reactive({ x: 0, y: 0 })
const currentItem = ref<NavItem | null>(null)
const currentClickedItem = ref(0)
const editModal = ref(false)
const editInput = ref('')
const searchInputRef = ref<HTMLInputElement | null>(null)
const pinnedIds = ref<string[]>(getPinned())
// 新增导航卡片延迟挂载，与导航列表一起交错入场
const addCardReady = ref(false)

const visible = ref(false)
const navData = reactive<NavItem>({
  menu_link: '',
  menu_name: '',
  tag: '',
  menu_icon: ''
})
const formNav = ref<any>(null)
const navcards = ref<any>(null)
const searchRef = useTemplateRef('searchRef')
const searchShellRef = useTemplateRef('searchShellRef')

const nav: any = $store.nav.useNavStore()
const usrLog = $store.log.useLogStore()

const contentStyle = ref<Record<string, string>>({})
const navStyle = ref<Record<string, string>>({})
const searchPanelStyle = ref<Record<string, string>>({})
const searchWord = ref('')
const broswer = ref(localStorage.getItem(ENGINE_KEY) || 'bing')
const searchItems = ref<SearchItem[]>([])
const selecedIdx = ref(0)
const searchBox = ref(false)
const navlist = ref<NavItem[]>([])
const tagList = ref<TagItem[]>([])
const options = ref<SearchEngineOption[]>([
  {
    label: '必应',
    value: 'bing',
    url: 'https://cn.bing.com/search?q='
  },
  {
    label: '百度',
    value: 'baidu',
    url: 'https://www.baidu.com/s?wd='
  },
  {
    label: '谷歌',
    value: 'google',
    url: 'https://www.google.com/search?q='
  },
  {
    label: '翻译',
    value: 'trans',
    url: 'https://translate.volcengine.com?text='
  },
])

let cloneNavlist: NavItem[] = []
let timer: ReturnType<typeof setTimeout> | null = null
const defaultTagName = '全部'
const addCardColor = getRandomDarkColor()

const currentSearchOption = computed(() => {
  return options.value.find(item => item.value === broswer.value) ?? options.value[0]
})

// 常用标签（新增导航弹窗快捷选择）
const quickTagOptions = computed(() => {
  const set = new Set<string>()
  cloneNavlist.forEach(item => {
    if (item.tag) set.add(item.tag)
  })
  return Array.from(set).slice(0, 8)
})

function isPinned(item?: NavItem | null): boolean {
  if (!item?.nid) return false
  return pinnedIds.value.includes(String(item.nid))
}

// 记忆搜索引擎选择
watch(broswer, (v) => {
  localStorage.setItem(ENGINE_KEY, v)
})

watch(searchWord, handleInput)
watch(() => usrLog.isLogin, (newVal: any) => {
  if (newVal) {
    getNavList()
  } else {
    navlist.value = []
    cloneNavlist = []
    resetTagList()
  }
})

function formatTag(tag: string) {
  return (tag || '').slice(0, 4)
}

function createDefaultTag(): TagItem {
  return {
    name: defaultTagName,
    color: getRandomDarkColor(30, 128),
    checked: true,
    count: 0
  }
}

// ---------- 本地访问统计与置顶 ----------
function getHitsMap(): Record<string, number> {
  try {
    return JSON.parse(localStorage.getItem(HITS_KEY) || '{}')
  } catch {
    return {}
  }
}

function getPinned(): string[] {
  try {
    const list = JSON.parse(localStorage.getItem(PINNED_KEY) || '[]')
    return Array.isArray(list) ? list : []
  } catch {
    return []
  }
}

function hitNav(item: NavItem) {
  if (!item.nid) return
  const key = String(item.nid)
  const hits = getHitsMap()
  hits[key] = (hits[key] || 0) + 1
  localStorage.setItem(HITS_KEY, JSON.stringify(hits))
  // 轻微延迟后按热度重排（配合 TransitionGroup 平滑过渡）
  window.setTimeout(() => {
    navlist.value = sortNavList(navlist.value)
  }, 320)
}

function togglePin() {
  if (!currentItem.value?.nid) return
  const key = String(currentItem.value.nid)
  let pins = getPinned()
  if (pins.includes(key)) {
    pins = pins.filter(p => p !== key)
    $msg.info('已取消置顶')
  } else {
    pins.unshift(key)
    $msg.success('已置顶，将优先展示')
  }
  localStorage.setItem(PINNED_KEY, JSON.stringify(pins))
  pinnedIds.value = pins
  navlist.value = sortNavList(navlist.value)
}

function cleanupNavLocal(nid?: number) {
  if (!nid) return
  const key = String(nid)
  pinnedIds.value = pinnedIds.value.filter(p => p !== key)
  localStorage.setItem(PINNED_KEY, JSON.stringify(pinnedIds.value))
  const hits = getHitsMap()
  if (hits[key] !== undefined) {
    delete hits[key]
    localStorage.setItem(HITS_KEY, JSON.stringify(hits))
  }
}

function sortNavList(list: NavItem[]): NavItem[] {
  const hits = getHitsMap()
  const pins = getPinned()
  return [...list].sort((a, b) => {
    const aPin = pins.includes(String(a.nid)) ? 1 : 0
    const bPin = pins.includes(String(b.nid)) ? 1 : 0
    if (aPin !== bPin) return bPin - aPin
    return (hits[String(b.nid)] || 0) - (hits[String(a.nid)] || 0)
  })
}

// ---------- 手动 FLIP 重排动画 ----------
// TransitionGroup 的内置 move 在"有删除"场景下会因 leave 元素占位导致位置计算错误，
// 这里在数据变化后自己实现 FLIP：记录旧位置 → 等待渲染与 leave 移除（双 rAF）→ 动画到新位置
const flipRects = new Map<string, DOMRect>()

function recordFlipPositions() {
  flipRects.clear()
  const el = navcards.value?.$el as HTMLElement | undefined
  if (!el) return
  el.querySelectorAll<HTMLElement>('.nav-card-wrap').forEach(wrap => {
    const nid = wrap.getAttribute('data-nid')
    if (nid) flipRects.set(nid, wrap.getBoundingClientRect())
  })
}

function playFlip() {
  // 三 rAF：确保渲染完成 + 被删除的卡片（leave 流程）已从 DOM 移除后再计算位置。
  // 若只等两帧，FLIP 会与 leave 移除同帧竞争（FLIP 先执行），
  // 被删卡片仍占位导致剩余卡片的新位置计算错误（dx=0），动画被跳过。
  requestAnimationFrame(() => requestAnimationFrame(() => requestAnimationFrame(() => {
    const el = navcards.value?.$el as HTMLElement | undefined
    if (!el) return

    el.querySelectorAll<HTMLElement>('.nav-card-wrap').forEach(wrap => {
      const nid = wrap.getAttribute('data-nid')
      const old = nid ? flipRects.get(nid) : undefined
      if (!old) return

      const now = wrap.getBoundingClientRect()
      const dx = old.left - now.left
      const dy = old.top - now.top
      if (!dx && !dy) return

      // Invert：瞬移到旧位置
      wrap.style.transition = 'none'
      wrap.style.transform = `translate(${dx}px, ${dy}px)`
      wrap.getBoundingClientRect() // 强制 reflow

      // Play：恢复过渡，动画到新位置
      wrap.style.transition = 'transform 0.45s cubic-bezier(0.22, 1, 0.36, 1)'
      wrap.style.transform = ''

      const cleanup = () => {
        wrap.style.transition = ''
        wrap.style.transform = ''
        wrap.removeEventListener('transitionend', cleanup)
      }
      wrap.addEventListener('transitionend', cleanup)
      // 兜底清理
      window.setTimeout(cleanup, 800)
    })
  })))
}

// pre-flush：DOM 更新前记录旧位置
watch(navlist, () => {
  recordFlipPositions()
  playFlip()
})

function resetTagList() {
  tagList.value = [createDefaultTag()]
}

function cancelSbox() {
  window.setTimeout(() => {
    searchBox.value = false
    searchItems.value = []
    selecedIdx.value = 0
  }, 200)
}

function handleInput() {
  const keyword = searchWord.value.trim().toLowerCase()
  if (!keyword) {
    searchItems.value = []
    searchBox.value = false
    selecedIdx.value = 0
    return
  }

  searchBox.value = true
  selecedIdx.value = 0
  const matchedItems = navlist.value.filter((item) =>
    item.menu_name?.toLowerCase().includes(keyword) ||
    item.menu_link?.toLowerCase().includes(keyword) ||
    formatTag(item.tag).toLowerCase().includes(keyword)
  )

  searchItems.value = [
    {
      menu_name: `在${currentSearchOption.value.label}中搜索“${searchWord.value}”`,
      menu_link: `${currentSearchOption.value.url}${searchWord.value}`,
      tag: ''
    },
    ...matchedItems.map(item => ({
      menu_name: item.menu_name,
      menu_link: item.menu_link,
      tag: item.tag,
    }))
  ]

  updateSearchPanelPosition()
}

function handleWindowKeydown(e: KeyboardEvent) {
  if (e.isComposing) return
  const target = e.target as HTMLElement | null
  const isTyping = !!target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)

  // Esc：逐层关闭浮层
  if (e.key === 'Escape') {
    if (menuShow.value) {
      menuShow.value = false
      return
    }
    if (editModal.value) {
      handleItemCancel()
      return
    }
    if (visible.value) {
      navCancel()
      return
    }
    if (searchBox.value) {
      searchBox.value = false
      searchItems.value = []
      selecedIdx.value = 0
      searchInputRef.value?.blur()
    }
    return
  }

  // Ctrl/Cmd + K 或 非输入状态下按 /：聚焦搜索框
  if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') {
    e.preventDefault()
    searchInputRef.value?.focus()
    return
  }
  if (e.key === '/' && !isTyping) {
    e.preventDefault()
    searchInputRef.value?.focus()
    return
  }

  // 搜索建议面板键盘导航
  if (!searchBox.value) return

  if (e.key === 'ArrowDown') {
    e.preventDefault()
    selecedIdx.value = selecedIdx.value < searchItems.value.length - 1 ? selecedIdx.value + 1 : 0
    return
  }

  if (e.key === 'ArrowUp') {
    e.preventDefault()
    selecedIdx.value = selecedIdx.value > 0 ? selecedIdx.value - 1 : searchItems.value.length - 1
    return
  }

  if (e.key === 'Enter') {
    // 弹窗打开时不拦截回车
    if (editModal.value || visible.value) return
    e.preventDefault()
    if (searchItems.value.length > 0) {
      goExtra(searchItems.value[selecedIdx.value].menu_link)
      searchBox.value = false
      selecedIdx.value = 0
    } else if (searchWord.value.trim()) {
      search()
    }
  }
}

const hideMenu = () => {
  removeTimer()
  timer = window.setTimeout(() => {
    menuShow.value = false
  }, 2000)
}

const removeTimer = () => {
  if (timer) {
    clearTimeout(timer)
    timer = null
  }
}

function handdleMenuItem(type: number) {
  menuShow.value = false
  currentClickedItem.value = type

  if (type === 4) {
    if (!currentItem.value) return

    $modal({
      title: '删除导航',
      content: `确定要删除【${currentItem.value.menu_name}】吗？删除后不可恢复。`,
      cancelText: '取消',
      submitText: '删除',
      handdleSubmit: async () => {
        try {
          const res = await $http.nav.deleteNav(currentItem.value?.nid)
          if (res.code === 200) {
            $msg.success('删除成功')
            cleanupNavLocal(currentItem.value?.nid)
            getNavList()
          }
        } catch {
          $msg.error('删除失败，请稍后再试')
        }
      }
    })
    return
  }

  // 复制链接
  if (type === 5) {
    copyLink()
    return
  }

  // 置顶 / 取消置顶
  if (type === 6) {
    togglePin()
    return
  }

  // 重新加载图标
  if (type === 7) {
    retryIcon()
    return
  }

  editModal.value = true
}

async function copyLink() {
  const link = currentItem.value?.menu_link
  if (!link) return

  try {
    await navigator.clipboard.writeText(link)
    $msg.success('链接已复制')
  } catch {
    // 兼容非 https 环境
    const textarea = document.createElement('textarea')
    textarea.value = link
    textarea.style.position = 'fixed'
    textarea.style.opacity = '0'
    document.body.appendChild(textarea)
    textarea.select()
    document.execCommand('copy')
    document.body.removeChild(textarea)
    $msg.success('链接已复制')
  }
}

async function retryIcon() {
  const item = currentItem.value
  if (!item?.menu_link) return

  try {
    const res = await $http.mix.getIcon({ url: item.menu_link })
    if (res.code === 200 && res.data?.url) {
      item.menu_icon = res.data.url
      item.icon_error = false
      const target = navlist.value.find(i => i.nid === item.nid)
      if (target) {
        target.menu_icon = res.data.url
        target.icon_error = false
      }
      $msg.success('图标已更新')
    } else {
      $msg.error('图标获取失败，可手动填写图标链接')
    }
  } catch {
    $msg.error('图标获取失败，可手动填写图标链接')
  }
}

function handleItemCancel() {
  editModal.value = false
  editInput.value = ''
}

async function handleItemSubmit() {
  const value = editInput.value.trim()
  if (!value) {
    $msg.error('请输入修改内容')
    return
  }

  if (!currentItem.value?.nid) return

  const updateData: Partial<NavItem> = {}
  if (currentClickedItem.value === 1) {
    updateData.menu_name = value
  } else if (currentClickedItem.value === 2) {
    updateData.menu_link = value
  } else if (currentClickedItem.value === 3) {
    updateData.tag = formatTag(value)
  }

  try {
    const res = await $http.nav.editNav(currentItem.value.nid, updateData)
    if (res.code === 200) {
      $msg.success('修改成功')
      handleItemCancel()
      getNavList()
    }
  } catch {
    $msg.error('修改失败，请稍后再试')
  }
}

async function getNavList() {
  const userinfo = $cookies.get('userinfo')
  if (!userinfo) {
    navlist.value = []
    cloneNavlist = []
    resetTagList()
    addCardReady.value = true
    return
  }

  try {
    const res = await $http.nav.getNavList()
    addCardReady.value = true
    if (!res.data) return

    resetTagList()
    const countMap: Record<string, number> = {}
    res.data.forEach((item: NavItem) => {
      const tag = formatTag(item.tag)
      countMap[tag] = (countMap[tag] || 0) + 1
    })

    const normalizedList = res.data.map((item: NavItem) => {
      const normalizedTag = formatTag(item.tag)

      if (!tagList.value.find(tag => tag.name === normalizedTag)) {
        tagList.value.push({
          name: normalizedTag,
          color: getRandomDarkColor(30, 128),
          checked: false,
          count: 0
        })
      }

      return {
        ...item,
        tag: normalizedTag,
        icon_error: false,
        first: item.menu_name.charAt(0) || '',
        color: getRandomDarkColor()
      }
    })

    // 统计各标签数量
    tagList.value.forEach(tag => {
      tag.count = tag.name === defaultTagName ? normalizedList.length : (countMap[tag.name] || 0)
    })

    cloneNavlist = deepclone(normalizedList)
    navlist.value = sortNavList(normalizedList)
  } catch {
    addCardReady.value = true
  }
}

function getItemColor(item: NavItem) {
  const tag = tagList.value.find((currentTag) => currentTag.name === formatTag(item.tag))
  return tag ? tag.color : getRandomDarkColor(30, 128)
}

function handdleTagClick(tag: TagItem) {
  if(tag.name == "全部" && tag.checked == true) return
  tag.checked = !tag.checked

  if (tag.name === defaultTagName) {
    tagList.value.forEach((currentTag) => {
      if (currentTag.name !== defaultTagName) {
        currentTag.checked = false
      }
    })
    navlist.value = sortNavList(cloneNavlist)
    return
  }

  const allTag = tagList.value.find((currentTag) => currentTag.name === defaultTagName)
  if (allTag) allTag.checked = false

  const selectedTags = tagList.value
    .filter((currentTag) => currentTag.checked && currentTag.name !== defaultTagName)
    .map((currentTag) => currentTag.name)

  if (selectedTags.length === 0) {
    if (allTag) allTag.checked = true
    navlist.value = sortNavList(cloneNavlist)
    return
  }

  navlist.value = sortNavList(cloneNavlist.filter((item) => selectedTags.includes(formatTag(item.tag))))
}

function imgErr(index: number) {
  if (navlist.value[index]) {
    navlist.value[index].icon_error = true
  }
}

function goExtra(link: string) {
  // 记录点击热度
  const matched = navlist.value.find(item => item.menu_link === link)
  if (matched) hitNav(matched)
  window.open(link, '_blank')
}

function updateSearchPanelPosition() {
  window.requestAnimationFrame(() => {
    const rect = searchShellRef.value?.getBoundingClientRect()
    if (!rect) return

    const viewportPadding = 24
    const offsetLeft = 112
    const left = Math.max(viewportPadding, rect.left + offsetLeft)
    const availableWidth = Math.max(280, window.innerWidth - left - viewportPadding)
    const width = Math.min(672, availableWidth, Math.max(320, rect.width - offsetLeft))
    const availableHeight = Math.max(180, window.innerHeight - rect.bottom - 32)

    searchPanelStyle.value = {
      left: `${Math.round(left)}px`,
      top: `${Math.round(rect.bottom + 14)}px`,
      width: `${Math.round(width)}px`,
      maxHeight: `${Math.round(Math.min(availableHeight, 420))}px`
    }
  })
}

function search() {
  const keyword = searchWord.value.trim()
  if (!keyword) return
  goExtra(`${currentSearchOption.value.url}${keyword}`)
}

function getRandomDarkColor(min = 30, max = 128): string {
  const normalizedMin = Math.max(0, min)
  const normalizedMax = Math.max(normalizedMin + 1, Math.min(255, max))
  const randomChannel = () => Math.floor(Math.random() * (normalizedMax - normalizedMin + 1)) + normalizedMin
  const toHex = (value: number) => value.toString(16).padStart(2, '0')

  return `#${toHex(randomChannel())}${toHex(randomChannel())}${toHex(randomChannel())}`
}

function addNav() {
  if (!$cookies.get('token')) {
    $msg.error('请先登录')
    return
  }
  visible.value = true
}

async function getIcon() {
  if (!navData.menu_link) return

  try {
    const res = await $http.mix.getIcon({
      url: navData.menu_link
    })

    if (res.code === 200) {
      navData.menu_icon = res.data.url
    }
  } catch {
    $msg.error('图标获取失败')
  }
}

function resetNav() {
  navData.menu_link = ''
  navData.menu_name = ''
  navData.tag = ''
  navData.menu_icon = ''
}

function navCancel() {
  visible.value = false
  resetNav()
}

async function navSubmit() {
  if (!navData.menu_link || !navData.menu_name || !navData.tag) {
    $msg.error('请输入链接、标签和名称')
    return
  }

  navData.tag = formatTag(navData.tag)

  try {
    const res = await $http.nav.addNav(navData)

    if (res.code === 200) {
      $msg.success('添加成功')
      visible.value = false
      getNavList()
    }
  } catch {
    $msg.error('添加失败，请稍后再试')
  }

  resetNav()
}

function handdleContextMenu(event: MouseEvent, item: NavItem) {
  menuShow.value = true
  MenuOptions.x = event.clientX
  MenuOptions.y = event.clientY
  currentItem.value = item
}

const handleResize = () => {
  const screenHeight = window.innerHeight

  contentStyle.value = {
    height: `${screenHeight - nav.navH}px`
  }
  const searchHeight: number = searchRef.value?.getBoundingClientRect().y || 0
  const searchH: number = searchRef.value?.getBoundingClientRect().height || 0
  navStyle.value = {
    height: `${window.innerHeight - (searchHeight + searchH)}px`
  }

  if (searchBox.value) {
    updateSearchPanelPosition()
  }
}

onMounted(() => {
  resetTagList()
  contentStyle.value = {
    height: `${window.innerHeight - nav.navH}px`
  }
  const navcardsEl = navcards.value?.$el as HTMLElement | undefined
  navStyle.value = {
    height: `${window.innerHeight - (navcardsEl?.getBoundingClientRect().y || 0)}px`
  }
  searchInputRef.value?.focus()


  getNavList()
  // 兜底：即使导航请求异常，也保证新增卡片最终出现
  window.setTimeout(() => {
    addCardReady.value = true
  }, 1200)
  window.addEventListener('resize', handleResize)
  window.addEventListener('scroll', handleResize, true)
  window.addEventListener('keydown', handleWindowKeydown)
})

onBeforeUnmount(() => {
  window.removeEventListener('resize', handleResize)
  window.removeEventListener('scroll', handleResize, true)
  window.removeEventListener('keydown', handleWindowKeydown)
  removeTimer()
})

watch(searchBox, (visible) => {
  if (visible) {
    updateSearchPanelPosition()
  }
})
</script>

<style scoped lang="less">
.modal-form-item {
  :deep(.n-form-item-feedback-wrapper) {
    min-height: 18px;
  }
}

:deep(.n-layout) {
  background: transparent !important;
}

:deep(.n-layout-content) {
  background: transparent !important;
}

:deep(.n-layout-sider) {
  background: transparent !important;
  border-left: none !important;
  box-shadow: none !important;
}

:deep(.n-base-selection) {
  border: none !important;
  box-shadow: none !important;
  background: transparent !important;
}

:deep(.search-select .n-base-selection) {
  border-radius: 9999px !important;
  background: transparent !important;
  padding-left: 0.2rem;
  padding-right: 0.2rem;
  min-height: 40px !important;
  position: relative;
}

:deep(.search-select .n-base-selection__border),
:deep(.search-select .n-base-selection__state-border) {
  display: none !important;
}

:deep(.search-select .n-base-selection-overlay) {
  background: transparent !important;
  border-radius: 9999px !important;
}

:deep(.search-select .n-base-selection-label) {
  justify-content: center;
  min-height: 2.5rem !important;
  border-radius: 9999px !important;
  background: linear-gradient(135deg, rgba(255, 255, 255, 0.28), rgba(255, 255, 255, 0.08)) !important;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.36),
    inset -1px 0 0 rgba(255, 255, 255, 0.1);
  padding-right: 0.25rem !important;
}

:deep(.search-select .n-base-selection-input) {
  font-weight: 700;
  color: #42556b !important;
  letter-spacing: 0.04em;
}

:deep(.search-select .n-base-selection__suffix) {
  color: #6d839a !important;
  margin-right: 0.1rem;
}

:deep(.n-input) {
  --n-border: none !important;
  --n-border-hover: none !important;
  --n-border-focus: none !important;
  --n-box-shadow-focus: none !important;
  background: transparent !important;
}

:deep(.n-input__input-el),
:deep(.n-base-selection-label) {
  color: #314155 !important;
}

.glass-surface,
.glass-nav-card {
  position: relative;
  border: 1px solid rgba(255, 255, 255, 0.58) !important;
  background:
    linear-gradient(135deg, rgba(255, 255, 255, 0.34), rgba(255, 255, 255, 0.16)),
    linear-gradient(180deg, rgba(241, 248, 252, 0.2), rgba(224, 236, 245, 0.08)) !important;
  box-shadow:
    0 1px 3px rgba(97, 121, 146, 0.05),
    0 14px 30px rgba(97, 121, 146, 0.09),
    0 36px 72px rgba(178, 128, 88, 0.07),
    inset 0 1px 0 rgba(255, 255, 255, 0.72),
    inset 0 -1px 0 rgba(255, 255, 255, 0.14) !important;
  backdrop-filter: blur(24px) saturate(165%);
  -webkit-backdrop-filter: blur(24px) saturate(165%);
}

.glass-surface {
  box-shadow:
    0 1px 3px rgba(97, 121, 146, 0.05),
    0 12px 28px rgba(97, 121, 146, 0.08),
    0 32px 64px rgba(178, 128, 88, 0.06),
    inset 0 1px 0 rgba(255, 255, 255, 0.7),
    inset 0 -1px 0 rgba(255, 255, 255, 0.12) !important;
}

.glass-floating-surface {
  box-shadow:
    0 2px 8px rgba(97, 121, 146, 0.08),
    0 18px 40px rgba(97, 121, 146, 0.13),
    0 56px 110px rgba(178, 128, 88, 0.11),
    inset 0 1px 0 rgba(255, 255, 255, 0.8),
    inset 0 -1px 0 rgba(255, 255, 255, 0.16) !important;
}

.search-shell {
  isolation: isolate;
}

.search-suggest-portal {
  isolation: isolate;
}

.search-suggest-panel {
  overflow: auto;
  transform-origin: top left;
  animation: search-panel-enter 180ms cubic-bezier(0.22, 1, 0.36, 1);
}

.search-suggest-item {
  min-height: 3.25rem;
}

@keyframes search-panel-enter {
  from {
    opacity: 0;
    transform: translateY(-6px) scale(0.98);
  }

  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

.glass-nav-card::before,
.glass-nav-card::after,
.glass-surface::before,
.glass-surface::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: inherit;
}

.glass-nav-card::before,
.glass-surface::before {
  background:
    linear-gradient(135deg, rgba(255, 255, 255, 0.34), rgba(255, 255, 255, 0.08) 48%, rgba(151, 197, 224, 0.08)),
    radial-gradient(circle at top left, rgba(255, 255, 255, 0.45), transparent 42%);
  opacity: 0.95;
}

.glass-nav-card::after,
.glass-surface::after {
  inset: 1px;
  border: 1px solid rgba(255, 255, 255, 0.24);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.14), transparent 36%, rgba(255, 255, 255, 0.08));
}

.glass-nav-card:hover {
  border-color: rgba(255, 255, 255, 0.74) !important;
  box-shadow:
    0 2px 6px rgba(97, 121, 146, 0.06),
    0 20px 44px rgba(97, 121, 146, 0.13),
    0 52px 100px rgba(178, 128, 88, 0.1),
    inset 0 1px 0 rgba(255, 255, 255, 0.8),
    inset 0 -1px 0 rgba(255, 255, 255, 0.18) !important;
}

.glass-nav-card--warm {
  background:
    linear-gradient(135deg, rgba(255, 249, 242, 0.4), rgba(255, 245, 231, 0.2)),
    linear-gradient(180deg, rgba(255, 236, 211, 0.14), rgba(244, 225, 204, 0.06)) !important;
}

:deep(.glass-nav-card > .n-card__content) {
  position: relative;
  z-index: 1;
  background: transparent !important;
}

.glass-surface > * {
  position: relative;
  z-index: 1;
}

:global(.search-select-menu) {
  position: relative;
  overflow: hidden;
  padding: 6px;
  border: 1px solid rgba(255, 255, 255, 0.58) !important;
  border-radius: 22px !important;
  background:
    linear-gradient(135deg, rgba(255, 255, 255, 0.34), rgba(255, 255, 255, 0.16)),
    linear-gradient(180deg, rgba(241, 248, 252, 0.2), rgba(224, 236, 245, 0.08)) !important;
  box-shadow:
    0 2px 8px rgba(97, 121, 146, 0.08),
    0 18px 40px rgba(97, 121, 146, 0.13),
    0 56px 110px rgba(178, 128, 88, 0.11),
    inset 0 1px 0 rgba(255, 255, 255, 0.8),
    inset 0 -1px 0 rgba(255, 255, 255, 0.16) !important;
  backdrop-filter: blur(24px) saturate(165%);
  -webkit-backdrop-filter: blur(24px) saturate(165%);
  transform-origin: top left;
  animation: search-panel-enter 180ms cubic-bezier(0.22, 1, 0.36, 1);
}

:global(.search-select-menu::before),
:global(.search-select-menu::after) {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: inherit;
}

:global(.search-select-menu::before) {
  background:
    linear-gradient(135deg, rgba(255, 255, 255, 0.34), rgba(255, 255, 255, 0.08) 48%, rgba(151, 197, 224, 0.08)),
    radial-gradient(circle at top left, rgba(255, 255, 255, 0.45), transparent 42%);
  opacity: 0.95;
}

:global(.search-select-menu::after) {
  inset: 1px;
  border: 1px solid rgba(255, 255, 255, 0.24);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.14), transparent 36%, rgba(255, 255, 255, 0.08));
}

:global(.search-select-menu .n-base-select-menu-option-wrapper),
:global(.search-select-menu .n-base-select-menu__empty) {
  position: relative;
  z-index: 1;
}

:global(.search-select-menu .n-base-select-menu-option-wrapper) {
  padding: 0 !important;
}

:global(.search-select-menu .n-base-select-option) {
  min-height: 2.45rem;
  margin: 0;
  padding-left: 0.7rem !important;
  padding-right: 0.7rem !important;
  border-radius: 16px;
  color: #475569;
  font-weight: 700;
  letter-spacing: 0;
  transition:
    transform 0.2s ease,
    background-color 0.2s ease,
    color 0.2s ease,
    box-shadow 0.2s ease,
    border-color 0.2s ease;
}

:global(.search-select-menu .n-base-select-option:not(.n-base-select-option--selected).n-base-select-option--pending) {
  transform: none;
  background: rgba(255, 255, 255, 0.62);
  box-shadow:
    0 2px 6px rgba(120, 144, 168, 0.08),
    inset 0 1px 0 rgba(255, 255, 255, 0.45);
}

:global(.search-select-menu .n-base-select-option--selected) {
  background: linear-gradient(135deg, rgba(255, 240, 246, 0.98), rgba(255, 233, 223, 0.96));
  color: #364152;
  box-shadow:
    0 3px 8px rgba(236, 102, 171, 0.1),
    0 10px 18px rgba(236, 102, 171, 0.08),
    inset 0 1px 0 rgba(255, 255, 255, 0.52);
}

:global(.search-select-menu .n-base-select-option--selected .n-base-select-option__content),
:global(.search-select-menu .n-base-select-option--selected .n-base-select-option__check) {
  color: inherit;
}

:global(.search-select-menu .n-base-select-option__content) {
  display: block;
  position: relative;
  z-index: 1;
}

:global(.search-select-menu .n-base-select-option__check) {
  display: none !important;
}

:global(.search-select-menu .n-base-select-menu__empty) {
  padding: 0.6rem 0.4rem;
  color: #64748b;
}

:deep(.modal-input .n-input) {
  background: #ffffff !important;
  border: 1px solid rgba(198, 212, 226, 0.92) !important;
  border-radius: 16px !important;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.95),
    0 2px 6px rgba(101, 126, 150, 0.05),
    0 10px 22px rgba(101, 126, 150, 0.07);
}

:deep(.modal-input .n-input:hover) {
  border-color: rgba(156, 183, 210, 0.98) !important;
}

:deep(.modal-input .n-input.n-input--focus) {
  border-color: rgba(236, 102, 171, 0.65) !important;
  box-shadow:
    0 0 0 4px rgba(236, 102, 171, 0.12),
    0 2px 6px rgba(101, 126, 150, 0.06),
    0 12px 24px rgba(101, 126, 150, 0.09) !important;
}

:deep(.modal-input .n-input__input-el) {
  color: #24364a !important;
}

:deep(.modal-input .n-input__placeholder) {
  color: #93a2b3 !important;
}

:deep(.n-card > .n-card__content) {
  height: 100%;
}

/* ===== 卡片入场 / 重排动画 ===== */
.nav-card-enter-active {
  transition:
    opacity 0.5s cubic-bezier(0.22, 1, 0.36, 1),
    transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
  transition-delay: calc(var(--i, 0) * 42ms);
}

.nav-card-enter-from {
  opacity: 0;
  transform: translateY(22px) scale(0.94);
}

/* wrap 容器无任何 transition —— Vue 检测不到 transform 过渡，
   从而禁用其内置 move 逻辑（有删除时 onUpdated 同步执行会因占位计算错误），
   重排动画由手动 FLIP（watch navlist + 双 rAF）接管 */
.nav-card-leave-active {
  transition: none !important;
}

/* ===== 卡片 hover 增强 ===== */
.glass-nav-card:hover .grid-image {
  transform: translateY(-3px) scale(1.08) rotate(-3deg);
  box-shadow:
    0 2px 6px rgba(97, 121, 146, 0.08),
    0 14px 26px rgba(97, 121, 146, 0.14),
    inset 0 1px 0 rgba(255, 255, 255, 0.95);
}

.card-name {
  transition: color 0.25s ease;
}

.glass-nav-card:hover .card-name {
  background: linear-gradient(135deg, #ec66ab, #f48a6f);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.glass-nav-card--warm:hover .add-card-plus {
  transform: rotate(90deg) scale(1.08);
}

/* ===== 搜索框聚焦发光 ===== */
.search-shell:focus-within .glass-surface {
  border-color: rgba(236, 102, 171, 0.42) !important;
  box-shadow:
    0 0 0 4px rgba(236, 102, 171, 0.1),
    0 2px 6px rgba(97, 121, 146, 0.05),
    0 16px 36px rgba(97, 121, 146, 0.12),
    inset 0 1px 0 rgba(255, 255, 255, 0.7) !important;
}

/* ===== 快捷键徽章 ===== */
.search-kbd {
  display: inline-flex;
  align-items: center;
  padding: 0.2rem 0.6rem;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: rgba(100, 116, 139, 0.85);
  border: 1px solid rgba(255, 255, 255, 0.65);
  border-radius: 9999px;
  background: linear-gradient(135deg, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.24));
  box-shadow:
    0 2px 6px rgba(97, 121, 146, 0.08),
    inset 0 1px 0 rgba(255, 255, 255, 0.7);
  backdrop-filter: blur(12px);
}

.kbd {
  display: inline-flex;
  min-width: 1.05rem;
  height: 1.05rem;
  align-items: center;
  justify-content: center;
  margin: 0 1px;
  padding: 0 0.3rem;
  font-size: 10px;
  font-weight: 700;
  color: #64748b;
  border: 1px solid rgba(148, 163, 184, 0.35);
  border-radius: 0.3rem;
  background: rgba(255, 255, 255, 0.65);
  box-shadow: 0 1px 2px rgba(100, 116, 139, 0.1);
}

/* ===== 右键菜单 ===== */
.menu-pop {
  min-width: 11.5rem;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.62);
  background:
    linear-gradient(135deg, rgba(255, 255, 255, 0.42), rgba(255, 255, 255, 0.24)),
    linear-gradient(180deg, rgba(241, 248, 252, 0.26), rgba(224, 236, 245, 0.1)) !important;
  backdrop-filter: blur(26px) saturate(170%);
  -webkit-backdrop-filter: blur(26px) saturate(170%);
  transform-origin: top left;
  animation: menu-enter 160ms cubic-bezier(0.22, 1, 0.36, 1);
}

.menu-head {
  padding: 0.65rem 1rem 0.6rem;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: #475569;
  border-bottom: 1px solid rgba(255, 255, 255, 0.55);
  background: linear-gradient(135deg, rgba(255, 240, 246, 0.5), rgba(255, 233, 223, 0.34));
}

.menu-item {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.62rem 1rem;
  font-size: 13.5px;
  font-weight: 600;
  color: #475569;
  transition:
    background-color 0.18s ease,
    color 0.18s ease,
    padding-left 0.18s ease;
}

.menu-item:hover {
  background: linear-gradient(135deg, rgba(255, 255, 255, 0.75), rgba(255, 255, 255, 0.45));
  color: #ec66ab;
  padding-left: 1.2rem;
}

.menu-item--danger {
  color: #e05d5d;
}

.menu-item--danger:hover {
  background: rgba(255, 236, 236, 0.85);
  color: #d94040;
}

.menu-icon {
  width: 1.05rem;
  height: 1.05rem;
  flex-shrink: 0;
  opacity: 0.85;
}

.menu-divider {
  height: 1px;
  margin: 0.3rem 0.9rem;
  background: linear-gradient(90deg, rgba(148, 163, 184, 0.28), rgba(148, 163, 184, 0.08));
}

@keyframes menu-enter {
  from {
    opacity: 0;
    transform: translateY(-6px) scale(0.96);
  }

  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

/* ===== 标签交互增强 ===== */
.nav-tag {
  transition:
    transform 0.18s cubic-bezier(0.22, 1, 0.36, 1),
    background-color 0.2s ease,
    box-shadow 0.2s ease,
    color 0.2s ease;
}

.nav-tag:hover {
  transform: translateY(-1.5px);
}

.nav-tag:active {
  transform: scale(0.92);
}

/* ===== 常用标签快捷选择 ===== */
.quick-tag {
  padding: 0.28rem 0.75rem;
  font-size: 12px;
  font-weight: 600;
  color: #64748b;
  border: 1px solid rgba(203, 213, 225, 0.9);
  border-radius: 9999px;
  background: rgba(255, 255, 255, 0.72);
  cursor: pointer;
  transition:
    transform 0.18s ease,
    border-color 0.18s ease,
    color 0.18s ease,
    background-color 0.18s ease,
    box-shadow 0.18s ease;
}

.quick-tag:hover {
  transform: translateY(-1px);
  border-color: rgba(236, 102, 171, 0.55);
  color: #ec66ab;
  box-shadow:
    0 2px 6px rgba(236, 102, 171, 0.1),
    0 6px 14px rgba(236, 102, 171, 0.08);
}

.quick-tag--active {
  color: #fff;
  border-color: transparent;
  background: linear-gradient(135deg, #ec66ab, #f48a6f);
  box-shadow:
    0 3px 8px rgba(236, 102, 171, 0.2),
    0 10px 18px rgba(236, 102, 171, 0.14);
}

/* ===== 置顶星标 ===== */
.nav-pin {
  animation: pin-in 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}

@keyframes pin-in {
  from {
    opacity: 0;
    transform: scale(0) rotate(-90deg);
  }

  to {
    opacity: 1;
    transform: scale(1) rotate(0);
  }
}

:deep(.nav-tag) {
  margin: 0 !important;
  box-sizing: border-box;
  border: none !important;
  outline: none !important;
  border-radius: 9999px !important;
  overflow: hidden;
}

:deep(.nav-tag .beyond-tag__text) {
  font-weight: 600;
}

:deep(.nav-tag.devui-tag-item) {
  background: transparent !important;
  border: none !important;
  box-shadow: none !important;
}

:deep(.nav-tag.devui-tag-item.active) {
  background: transparent !important;
  color: inherit !important;
  border: none !important;
}

:deep(.nav-tag .devui-tag) {
  border: none !important;
  border-radius: 9999px !important;
  background: transparent !important;
  box-shadow: none !important;
}

:deep(.nav-tag .devui-tag__item) {
  border: none !important;
  border-radius: 9999px !important;
  background: transparent !important;
}

:deep(.nav-tag:hover),
:deep(.nav-tag:focus),
:deep(.nav-tag:active) {
  border: none !important;
  outline: none !important;
  box-shadow: none !important;
}

:deep(.navcard .n-card > .n-card__content) {
  padding: 0 !important;
  display: flex;
  justify-content: center;
  align-items: center;
}
</style>

