style: 优化右键菜单与弹窗交互样式

This commit is contained in:
2026-08-08 10:29:58 +08:00
parent 925ff669df
commit dae7f8edbb
5 changed files with 121 additions and 63 deletions
+31 -5
View File
@@ -1,6 +1,6 @@
<template>
<Teleport to="body" v-if="show">
<div class="fixed z-[99999]" :style="menuStyle">
<div ref="menuEl" class="fixed z-[99999] min-w-[10rem]" :style="menuStyle">
<slot></slot>
</div>
</Teleport>
@@ -16,10 +16,36 @@ const props = defineProps<{
options: any
}>()
let timer:any = null
const menuStyle = computed(() => {
return {
top: props.options.y + 'px',
left: props.options.x + 'px',
const menuEl = ref<HTMLElement | null>(null)
const pos = reactive({ x: 0, y: 0 })
const MARGIN = 12 // 距屏幕边缘的留白
const menuStyle = computed(() => ({
top: pos.y + 'px',
left: pos.x + 'px',
}))
// 根据真实菜单尺寸做边界钳制
function clampPosition() {
const x = props.options?.x || 0
const y = props.options?.y || 0
const w = menuEl.value?.offsetWidth || 200
const h = menuEl.value?.offsetHeight || 260
pos.x = Math.min(x, Math.max(MARGIN, window.innerWidth - w - MARGIN))
pos.y = Math.min(y, Math.max(MARGIN, window.innerHeight - h - MARGIN))
}
// 每次显示时,等菜单渲染出真实尺寸后再定位
watch(() => props.show, (v) => {
if (v) {
nextTick(() => {
// 双 rAF:确保 slot 内容已渲染出真实尺寸后再精确钳制
window.requestAnimationFrame(() => {
window.requestAnimationFrame(clampPosition)
})
})
}
})
//mark method
+15 -13
View File
@@ -160,22 +160,22 @@ const usrLog = $store.log.useLogStore()
const menuOptions = [
{
label: () => h(RouterLink, { to: '/home', class: 'flex items-center justify-center' }, { default: () => '首页' }),
label: () => h(RouterLink, { to: '/home',tag:'a', class: 'flex items-center justify-center' }, { default: () => '首页' }),
key: 'home',
icon: () => h(homeSvg)
},
{
label: () => h(RouterLink, { to: '/gallery', class: 'flex items-center justify-center' }, { default: () => '画廊' }),
label: () => h(RouterLink, { to: '/gallery',tag:'a', class: 'flex items-center justify-center' }, { default: () => '画廊' }),
key: 'gallery',
icon: () => h(picSvg)
},
{
label: () => h(RouterLink, { to: '/blog', class: 'flex items-center justify-center' }, { default: () => '文章' }),
label: () => h(RouterLink, { to: '/blog',tag:'a', class: 'flex items-center justify-center' }, { default: () => '文章' }),
key: 'blog',
icon: () => h(artiSvg)
},
{
label: () => h(RouterLink, { to: '/plink', class: 'flex items-center justify-center' }, { default: () => '友链' }),
label: () => h(RouterLink, { to: '/plink',tag:'a', class: 'flex items-center justify-center' }, { default: () => '友链' }),
key: 'plink',
icon: () => h(linkSvg)
},
@@ -330,18 +330,18 @@ const getLocation = () => {
navigator.geolocation.getCurrentPosition(
async (position) => {
locationInfo.value = '正在定位...';
latitude.value = position.coords.latitude;
longitude.value = position.coords.longitude;
const jw = `${longitude.value.toFixed(2)},${latitude.value.toFixed(2)}`;
const jw = `${longitude.value.toFixed(4)},${latitude.value.toFixed(4)}`;
await handdleJw(jw);
},
async (error) => {
switch (error.code) {
case error.PERMISSION_DENIED:
case error.POSITION_UNAVAILABLE:
case error.TIMEOUT:
locationInfo.value = '正在定位...';
break;
// 只有用户明确拒绝授权时才提示,其余情况(定位不可用/超时)静默走 IP 兜底
if (error.code === error.PERMISSION_DENIED) {
locationInfo.value = '未授权定位,正在用 IP 定位';
} else {
locationInfo.value = '正在定位...';
}
try {
@@ -356,8 +356,10 @@ const getLocation = () => {
},
{
enableHighAccuracy: false,
timeout: 5000,
maximumAge: 0
// 5s 太短,很多设备定位较慢容易误判为失败,放宽到 10s
timeout: 10000,
// 允许复用 1 分钟内的缓存定位,避免每次进入都强制重新定位
maximumAge: 60000
}
);
};
+8 -8
View File
@@ -19,14 +19,14 @@
把喜欢的画面收进一片更柔和的光里
</h1>
<p class="mt-[0.9rem] max-w-[42rem] leading-[1.75] text-[#66778b]">
输入关键词后按回车即可筛选图片
如果图片效果模糊的话可以点击图片查看原图
</p>
</div>
<div
class="mt-6 rounded-full border border-white/62 bg-[linear-gradient(135deg,rgba(255,249,241,0.76),rgba(236,245,243,0.72)),radial-gradient(circle_at_top_right,rgba(255,255,255,0.28),transparent_34%)] shadow-[inset_0_1px_0_rgba(255,255,255,0.55)] transition-[box-shadow,border-color] duration-300">
class="mt-6 rounded-full border border-white bg-[linear-gradient(135deg,rgba(255,249,241,0.76),rgba(236,245,243,0.72)),radial-gradient(circle_at_top_right,rgba(255,255,255,0.28),transparent_34%)] shadow-[0_0_0_2px_rgba(255,255,255,0.75),inset_0_1px_0_rgba(255,255,255,0.55)] transition-[box-shadow,border-color] duration-300">
<n-input class="gallery-search" round size="large" clearable v-model:value="kw" @keyup.enter="onSearch"
placeholder="输入关键词,按回车开始搜索">
placeholder="输入分享者 | 图片名,按回车开始搜索">
<template #suffix>
<n-icon class="cursor-pointer text-slate-500 transition-colors duration-200 hover:text-primary" size="large" @click="onSearch">
<icon-search />
@@ -37,7 +37,7 @@
</section>
<section
class="mx-auto mt-6 w-[82%] rounded-[30px] border border-white/62 bg-white/42 p-6 shadow-[0_1px_3px_rgba(128,118,126,0.05),0_14px_30px_rgba(128,118,126,0.09),0_36px_72px_rgba(215,26,68,0.07),inset_0_1px_0_rgba(255,255,255,0.7)] backdrop-blur-[18px]">
class="mx-auto mt-6 w-[82%] min-h-[80vh] rounded-[30px] border border-white/62 bg-white/42 p-6 shadow-[0_1px_3px_rgba(128,118,126,0.05),0_14px_30px_rgba(128,118,126,0.09),0_36px_72px_rgba(215,26,68,0.07),inset_0_1px_0_rgba(255,255,255,0.7)] backdrop-blur-[18px]">
<div class="mb-5 flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
<div>
<div class="text-[1.6rem] font-semibold text-[#5c3d46]">图片列表</div>
@@ -62,16 +62,16 @@
<LazyImg class="gallery-img overflow-hidden rounded-[12px]" :Pwidth="item.width" :Pheight="item.height"
:url="item.thumbnail" :preUrl="item.filepath" />
<div
class="pointer-events-none absolute left-0 right-0 top-0 z-10 truncate rounded-[12px] border-b border-white/22 bg-gradient-to-b from-[rgba(39,50,65,0.55)] to-transparent px-3 pb-3 pt-1.5 text-sm text-white opacity-0 backdrop-blur-[6px] transition-opacity duration-300 group-hover:opacity-100">
class="pointer-events-none absolute left-0 right-0 top-0 z-10 pt-1.5 px-3 text-center truncate rounded-[12px] bg-gradient-to-b from-[rgba(39,50,65,0.55)] to-transparent text-sm text-white opacity-0 backdrop-blur-[6px] transition-opacity duration-300 group-hover:opacity-100">
{{ item.filename }}
</div>
<div
class="absolute bottom-0 left-0 right-0 z-10 flex translate-y-[102%] items-center justify-between gap-3 rounded-[12px] border border-white/22 bg-[rgba(39,50,65,0.55)] px-3 py-1.5 text-white backdrop-blur-[12px] transition-transform duration-[320ms] [transition-timing-function:cubic-bezier(0.22,1,0.36,1)] group-hover:translate-y-0">
class="absolute bottom-0 left-0 right-0 z-10 flex translate-y-[102%] items-center justify-between gap-3 rounded-[12px] bg-[rgba(39,50,65,0.55)] px-3 py-0.5 text-white backdrop-blur-[12px] transition-transform duration-[320ms] [transition-timing-function:cubic-bezier(0.22,1,0.36,1)] group-hover:translate-y-0">
<div class="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-sm text-[#eef4f8]">
<span class="font-semibold text-[#ffd8cb]">{{ item.nickname }}</span> 分享
<span class="text-lg text-[#ffd8cb]">{{ item.nickname }}</span> 分享
</div>
<div
class="inline-flex flex-shrink-0 cursor-pointer items-center rounded-full bg-white/18 px-3.5 py-1 text-[0.86rem] font-semibold text-[#ffe3d3] transition-[background-color,transform] duration-200 hover:scale-105 hover:bg-white/28 hover:text-white active:scale-95"
class="inline-flex flex-shrink-0 cursor-pointer items-center rounded-full bg-white/18 text-sm px-2 py-0.5 text-[#ffe3d3] transition-[background-color,transform] duration-200 hover:scale-105 hover:bg-white/28 hover:text-white active:scale-95"
@click.stop="downloadFile(item.filepath)">
<icon-download class="mr-1 h-4 w-4" />
下载
+62 -37
View File
@@ -184,26 +184,26 @@
<maskX :visible="visible" :setVisible="navCancel">
<n-form
class="w-[500px] rounded-[28px] border border-white/80 bg-[radial-gradient(circle_at_top_left,rgba(135,196,235,0.04),transparent_34%),radial-gradient(circle_at_top_right,rgba(200,182,224,0.03),transparent_28%),linear-gradient(180deg,rgba(255,255,255,0.84),rgba(200,182,224,0.10))] p-8 shadow-[0_4px_12px_rgba(128,118,126,0.08),0_20px_44px_rgba(128,118,126,0.12),0_64px_120px_rgba(215,26,68,0.08)] backdrop-blur-[20px]"
class="modal-glass"
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]" />
<div class="modal-title">
<icon-star class="w-5 h-5 text-[#f0a13c]" />
新增导航
<icon-star class="w-5 text-[#f0a13c]" />
<icon-star class="w-5 h-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-input class="modal-origin" size="large" @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-input class="modal-origin" size="large" 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-input class="modal-origin" size="large" 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">
<div v-if="quickTagOptions.length" class="mt-8 flex flex-wrap items-center gap-1.5 pl-3">
<span class="text-xs text-slate-400">常用</span>
<span
v-for="t in quickTagOptions"
@@ -213,8 +213,8 @@
@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="请输入图标链接" />
<n-form-item class="h-8 modal-form-item" path="menu_icon">
<n-input class="modal-origin" size="large" 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"
@@ -264,18 +264,31 @@
</contextMenu>
<maskX :visible="editModal" :setVisible="handleItemCancel">
<div class="w-[500px] rounded-[28px] border border-white/80 bg-[radial-gradient(circle_at_top_left,rgba(135,196,235,0.04),transparent_34%),radial-gradient(circle_at_top_right,rgba(200,182,224,0.03),transparent_28%),linear-gradient(180deg,rgba(255,255,255,0.84),rgba(200,182,224,0.10))] p-8 shadow-[0_4px_12px_rgba(128,118,126,0.08),0_20px_44px_rgba(128,118,126,0.12),0_64px_120px_rgba(215,26,68,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 class="modal-glass">
<div class="modal-title">
<icon-pen class="w-5 h-5 text-primary" />
修改导航内容
</div>
<div class="mb-4 text-slate-500">
原内容
<span class="text-primary" v-if="currentClickedItem === MenuAction.RENAME">导航名称{{ currentItem?.menu_name }}</span>
<span class="text-primary" v-else-if="currentClickedItem === MenuAction.EDIT_LINK">导航链接{{ currentItem?.menu_link }}</span>
<span class="text-primary" v-else-if="currentClickedItem === MenuAction.EDIT_TAG">导航标签{{ currentItem?.tag }}</span>
<div class="text-sm text-center text-gray-600 my-5">
名称{{ currentItem?.menu_name }}
</div>
<n-input class="modal-input" v-model:value="editInput" placeholder="请输入修改内容" @keyup.enter="handleItemSubmit" />
<div class="modal-origin p-3">
<span class="modal-origin-label">原内容</span>
<span class="modal-origin-value" v-if="currentClickedItem === MenuAction.RENAME">名称{{ currentItem?.menu_name }}</span>
<span class="modal-origin-value" v-else-if="currentClickedItem === MenuAction.EDIT_LINK">链接{{ currentItem?.menu_link }}</span>
<span class="modal-origin-value" v-else-if="currentClickedItem === MenuAction.EDIT_TAG">标签{{ currentItem?.tag }}</span>
</div>
<div class="mb-2 px-1 text-xs font-semibold tracking-[0.04em] text-slate-400">请输入新内容</div>
<n-input
ref="editInputRef"
class="modal-origin"
size="large"
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(128,118,126,0.07),0_12px_24px_rgba(128,118,126,0.09)]" secondary @click="handleItemCancel">取消</n-button>
<n-button class="w-[48%] !h-11 !rounded-full !border-none !bg-[linear-gradient(135deg,#D71A44,#DB5F51)] !font-bold !tracking-[0.04em] shadow-[0_4px_10px_rgba(215,26,68,0.18),0_14px_28px_rgba(215,26,68,0.2)]" type="primary" @click="handleItemSubmit">确定</n-button>
@@ -359,6 +372,7 @@ const currentItem = ref<NavItem | null>(null)
const currentClickedItem = ref<MenuActionType | 0>(0)
const editModal = ref(false)
const editInput = ref('')
const editInputRef = ref<any>(null)
const searchInputRef = ref<HTMLInputElement | null>(null)
const pinnedIds = ref<string[]>(getPinned())
// 新增导航卡片延迟挂载,与导航列表一起交错入场
@@ -488,10 +502,7 @@ function hitNav(item: NavItem) {
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() {
@@ -523,13 +534,11 @@ function cleanupNavLocal(nid?: number) {
}
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)
return bPin - aPin
})
}
@@ -768,6 +777,10 @@ function handdleMenuItem(type: MenuActionType) {
}
editModal.value = true
// 弹窗打开后默认聚焦输入框
nextTick(() => {
editInputRef.value?.focus?.()
})
}
async function copyLink() {
@@ -1415,32 +1428,44 @@ watch(searchBox, (visible) => {
:deep(.modal-input .n-input) {
background: #ffffff !important;
border: 1px solid rgba(198, 212, 226, 0.92) !important;
border: 2px solid rgba(147, 176, 207, 1) !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);
0 0 0 3px rgba(147, 176, 207, 0.16),
inset 0 2px 6px rgba(101, 126, 150, 0.18),
0 2px 8px rgba(101, 126, 150, 0.1),
0 10px 22px rgba(101, 126, 150, 0.12);
transition:
border-color 0.2s ease,
box-shadow 0.2s ease;
}
:deep(.modal-input .n-input:hover) {
border-color: rgba(156, 183, 210, 0.98) !important;
border-color: rgba(117, 149, 187, 1) !important;
box-shadow:
0 0 0 3px rgba(117, 149, 187, 0.18),
inset 0 2px 6px rgba(101, 126, 150, 0.18),
0 4px 12px rgba(101, 126, 150, 0.14),
0 14px 30px rgba(101, 126, 150, 0.14);
}
:deep(.modal-input .n-input.n-input--focus) {
border-color: rgba(236, 102, 171, 0.65) !important;
border-color: rgba(236, 102, 171, 0.9) !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;
0 0 0 4px rgba(236, 102, 171, 0.22),
inset 0 1px 3px rgba(101, 126, 150, 0.06),
0 4px 10px rgba(236, 102, 171, 0.16),
0 14px 30px rgba(101, 126, 150, 0.12) !important;
}
:deep(.modal-input .n-input__input-el) {
color: #24364a !important;
color: #1f2f42 !important;
font-weight: 600;
}
:deep(.modal-input .n-input__placeholder) {
color: #93a2b3 !important;
color: #8aa0b7 !important;
font-weight: 400;
}
:deep(.n-card > .n-card__content) {
+5
View File
@@ -24,6 +24,11 @@ export default defineConfig({
"home-nav-card-warm": "bg-[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))]",
"home-search-select": "rounded-full bg-transparent px-0.5 py-0 min-h-[2.5rem] relative before:pointer-events-none before:absolute before:inset-[1px] before:rounded-[9999px] before:bg-[linear-gradient(135deg,rgba(255,255,255,0.28),rgba(255,255,255,0.08))] before:shadow-[inset_0_1px_0_rgba(255,255,255,0.36),inset_-1px_0_0_rgba(255,255,255,0.1)] before:content-['']",
"home-search-panel-animate": "overflow-auto origin-top motion-safe:[animation:home-search-panel-enter_180ms_cubic-bezier(0.22,1,0.36,1)_both]",
"modal-glass": "relative w-[500px] px-8 py-9 overflow-hidden rounded-[28px] border border-white/70 bg-[linear-gradient(145deg,rgba(255,255,255,0.55),rgba(255,255,255,0.22)),radial-gradient(circle_at_15%_0%,rgba(135,196,235,0.12),transparent_45%),radial-gradient(circle_at_85%_10%,rgba(200,182,224,0.1),transparent_40%),linear-gradient(180deg,rgba(248,251,253,0.9),rgba(244,248,252,0.75))] shadow-[0_4px_12px_rgba(97,121,146,0.08),0_24px_52px_rgba(97,121,146,0.14),0_64px_120px_rgba(215,26,68,0.1),inset_0_1px_0_rgba(255,255,255,0.85),inset_0_-1px_0_rgba(255,255,255,0.2)] backdrop-blur-[28px] saturate-[170%] before:pointer-events-none before:absolute before:inset-0 before:rounded-[inherit] before:bg-[linear-gradient(135deg,rgba(255,255,255,0.5),rgba(255,255,255,0.1)_45%,rgba(151,197,224,0.1)),radial-gradient(circle_at_top_left,rgba(255,255,255,0.55),transparent_42%)] before:opacity-[0.9] before:content-[''] after:pointer-events-none after:absolute after:inset-[1px] after:rounded-[inherit] after:border after:border-white/28 after:bg-[linear-gradient(180deg,rgba(255,255,255,0.2),transparent_34%,rgba(255,255,255,0.08))] after:content-[''] [&>*]:relative [&>*]:z-[1]",
"modal-title": "flex items-center justify-center gap-2 text-center text-xl font-extrabold tracking-[0.02em] text-[#1f2f42]",
"modal-origin": "flex items-center gap-2 mb-4 rounded-[14px] border border-[rgba(236,102,171,0.24)] bg-[linear-gradient(135deg,rgba(255,240,246,0.72),rgba(255,244,240,0.5))] shadow-[inset_0_1px_0_rgba(255,255,255,0.7),0_4px_12px_rgba(236,102,171,0.08)] text-[13.5px] text-[#475569]",
"modal-origin-label": "shrink-0 font-bold tracking-[0.03em] text-[#D71A44]",
"modal-origin-value": "truncate font-semibold text-[#1f2f42]",
},
preflights: [
{