mirror of
https://github.com/stardrophere/InsightRadar.git
synced 2026-06-06 00:39:21 +08:00
optimize+注释
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
<!-- 品牌 Logo:雷达扫描风格 SVG,带呼吸灯与旋转动画 -->
|
||||
<template>
|
||||
<div class="brand-logo-container">
|
||||
<svg class="insight-logo" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string | number
|
||||
options: { label: string; value: string | number }[]
|
||||
icon: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string | number): void
|
||||
}>()
|
||||
|
||||
const isOpen = ref(false)
|
||||
const selectRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const toggle = () => {
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
|
||||
const selectOption = (value: string | number) => {
|
||||
emit('update:modelValue', value)
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (selectRef.value && !selectRef.value.contains(event.target as Node)) {
|
||||
isOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="custom-select-wrapper" ref="selectRef">
|
||||
<i :class="['select-icon', icon]"></i>
|
||||
<div class="select-trigger" :class="{ 'is-open': isOpen }" @click="toggle">
|
||||
<span class="selected-label">
|
||||
{{ options.find(o => o.value === modelValue)?.label }}
|
||||
</span>
|
||||
<i class="fa-solid fa-chevron-down select-arrow"></i>
|
||||
</div>
|
||||
|
||||
<transition name="dropdown">
|
||||
<ul v-if="isOpen" class="select-dropdown">
|
||||
<li
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
class="select-option"
|
||||
:class="{ 'is-active': option.value === modelValue }"
|
||||
@click="selectOption(option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</li>
|
||||
</ul>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.custom-select-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.select-icon {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
color: var(--text-placeholder);
|
||||
font-size: 14px;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.select-arrow {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
color: var(--text-placeholder);
|
||||
font-size: 11px;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
transition: color 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.select-trigger {
|
||||
width: 100%;
|
||||
padding: 12px 34px 12px 38px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
background-color: var(--bg-input);
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.select-trigger:hover {
|
||||
border-color: var(--border-strong);
|
||||
background-color: var(--bg-surface);
|
||||
}
|
||||
|
||||
.select-trigger.is-open {
|
||||
border-color: var(--brand-primary);
|
||||
box-shadow: 0 0 0 3px var(--brand-primary-alpha);
|
||||
background-color: var(--bg-surface);
|
||||
}
|
||||
|
||||
.select-trigger.is-open .select-arrow {
|
||||
transform: rotate(180deg);
|
||||
color: var(--brand-primary);
|
||||
}
|
||||
|
||||
.custom-select-wrapper:hover .select-icon,
|
||||
.custom-select-wrapper:hover .select-arrow {
|
||||
color: var(--brand-primary);
|
||||
}
|
||||
|
||||
.select-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
left: 0;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
margin: 0;
|
||||
padding: 6px;
|
||||
list-style: none;
|
||||
background: var(--bg-surface);
|
||||
backdrop-filter: var(--backdrop-blur, blur(12px));
|
||||
-webkit-backdrop-filter: var(--backdrop-blur, blur(12px));
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
||||
z-index: 100;
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.select-option {
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
margin-bottom: 2px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.select-option:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.select-option:hover {
|
||||
background-color: var(--bg-input);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.select-option.is-active {
|
||||
background-color: var(--brand-primary-alpha);
|
||||
color: var(--brand-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 滚动条美化 */
|
||||
.select-dropdown::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.select-dropdown::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.select-dropdown::-webkit-scrollbar-thumb {
|
||||
background-color: var(--border-strong);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 过渡动画 */
|
||||
.dropdown-enter-active,
|
||||
.dropdown-leave-active {
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
.dropdown-enter-from,
|
||||
.dropdown-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
</style>
|
||||
@@ -4,17 +4,17 @@ import { useThemeStore } from '@/stores/theme'
|
||||
const themeStore = useThemeStore()
|
||||
|
||||
/**
|
||||
* 切换主题,使用 View Transitions API 实现高级扩散动画(如果浏览器支持)
|
||||
* 这种动画比之前像玩具一样的开关要高级得多,提供原生级的丝滑过渡
|
||||
* 切换主题:支持 View Transitions API 时使用点击位置扩散动画,
|
||||
* 否则直接切换
|
||||
*/
|
||||
function handleToggle(event: MouseEvent) {
|
||||
// 检查浏览器是否支持 document.startViewTransition 并且用户没有开启减弱动画
|
||||
// 检测浏览器是否支持 View Transitions 且用户未开启减弱动画
|
||||
const isAppearanceTransition = typeof document !== 'undefined' &&
|
||||
'startViewTransition' in document &&
|
||||
!window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
|
||||
if (!isAppearanceTransition) {
|
||||
// 降级处理:直接切换
|
||||
// 不支持时直接切换,无动画
|
||||
themeStore.toggleTheme()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- 统一事件卡片:展示标题、摘要、平台来源、排名轨迹,悬停展开图表 -->
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
Reference in New Issue
Block a user