This commit is contained in:
zhouwentao 2026-01-24 20:22:44 +08:00
parent b02d66cc2b
commit 8b9602ff4c
3 changed files with 84 additions and 101 deletions

View File

@ -20,11 +20,6 @@ const props = withDefaults(defineProps<Props>(), {
// Emits ()
const emit = defineEmits<{
(e: 'change', payload: { keyword: string, filters: FilterState }): void
// //
// (e: 'confirm', filters: FilterState): void;
// //
// (e: 'search', keyword: string): void;
}>()
// --- ---
@ -45,41 +40,10 @@ const filters: FilterConfig[] = [
//
const locations = [
'不限',
'北京',
'天津',
'河北',
'山西',
'内蒙古',
'辽宁',
'吉林',
'黑龙江',
'上海',
'江苏',
'浙江',
'安徽',
'福建',
'江西',
'山东',
'河南',
'湖北',
'湖南',
'广东',
'广西',
'海南',
'重庆',
'四川',
'贵州',
'云南',
'西藏',
'陕西',
'甘肃',
'青海',
'宁夏',
'新疆',
'台湾',
'香港',
'澳门',
'不限', '北京', '天津', '河北', '山西', '内蒙古', '辽宁', '吉林', '黑龙江',
'上海', '江苏', '浙江', '安徽', '福建', '江西', '山东', '河南', '湖北',
'湖南', '广东', '广西', '海南', '重庆', '四川', '贵州', '云南', '西藏',
'陕西', '甘肃', '青海', '宁夏', '新疆', '台湾', '香港', '澳门',
]
// --- ---
@ -95,19 +59,13 @@ const selectedFilters = reactive({
sort: '默认',
})
// selectedFilters
//
// --- ---
function getLabel(key: FilterKey) {
//
//
const map = { location: '位置', type: '类型', major: '专业', sort: '排序' }
return map[key]
}
const countSelected = computed(() => {
// 10
if (activeFilter.value === 'location') {
return selectedFilters.location !== '不限' ? 1 : 0
}
@ -115,7 +73,6 @@ const countSelected = computed(() => {
})
function shouldShowFilter(filter: FilterConfig) {
//
return filter.key !== 'sort' || (filter.key === 'sort' && props.sortEnabled)
}
@ -146,15 +103,12 @@ function clearCurrentFilter() {
//
function confirmSelection() {
emit('change', { keyword: searchQuery.value, filters: { ...selectedFilters } })
// 使 {...}
// emit('confirm', { ...selectedFilters });
activeFilter.value = null //
}
//
function handleSearch() {
emit('change', { keyword: searchQuery.value, filters: { ...selectedFilters } })
// emit('search', searchQuery.value);
}
// --- ---
@ -174,8 +128,9 @@ onUnmounted(() => {
</script>
<template>
<!-- 最外层容器用于定位下拉菜单和监听点击外部事件 -->
<div ref="containerRef" class="relative mx-auto max-w-5xl w-full select-none text-sm text-gray-600 font-sans">
<!-- 最外层容器 -->
<!-- Update: text-gray-600 -> dark:text-slate-300 -->
<div ref="containerRef" class="relative mx-auto w-full max-w-5xl select-none font-sans text-sm text-gray-600 dark:text-slate-300">
<!-- 顶部栏筛选按钮组 + 搜索框 -->
<div class="flex flex-wrap items-center justify-between gap-4">
<!-- 左侧4个下拉筛选器 -->
@ -187,14 +142,30 @@ onUnmounted(() => {
class="relative"
>
<!-- 筛选器按钮 -->
<!-- Update:
bg-white -> dark:bg-slate-800
border-gray-200 -> dark:border-slate-700
ring-blue-200 -> dark:ring-blue-900 (Focus ring 变深)
-->
<button
class="h-9 w-24 flex items-center justify-between border rounded bg-white px-3 transition-colors hover:border-blue-400"
:class="activeFilter === filter.key ? 'border-blue-500 ring-1 ring-blue-200' : 'border-gray-200'"
class="flex h-9 w-24 items-center justify-between rounded border px-3 transition-colors hover:border-blue-400"
:class="[
activeFilter === filter.key
? 'border-blue-500 ring-1 ring-blue-200 dark:ring-blue-900'
: 'border-gray-200 dark:border-slate-700',
'bg-white dark:bg-slate-800'
]"
@click="toggleFilter(filter.key)"
>
<span class="truncate">{{ getLabel(filter.key) }}</span>
<!-- 下箭头图标 -->
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 text-gray-400 transition-transform duration-200" :class="{ 'rotate-180': activeFilter === filter.key }" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<!-- Update: text-gray-400 -> dark:text-slate-500 -->
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-3 w-3 text-gray-400 transition-transform duration-200 dark:text-slate-500"
:class="{ 'rotate-180': activeFilter === filter.key }"
fill="none" viewBox="0 0 24 24" stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
@ -205,17 +176,19 @@ onUnmounted(() => {
<div class="flex flex-wrap items-center">
<div class="relative">
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<svg class="h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
<!-- Update: Icon 颜色 -->
<svg class="h-4 w-4 text-gray-400 dark:text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
</div>
<!-- Update: 输入框颜色适配 -->
<input
v-model="searchQuery"
type="text"
class="h-9 w-50 border border-gray-300 rounded-l bg-white py-2 pl-9 pr-4 text-gray-700 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-200 placeholder-gray-400"
class="h-9 w-50 rounded-l border border-gray-300 py-2 pl-9 pr-4 text-gray-700 placeholder-gray-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-200 dark:bg-slate-800 dark:border-slate-700 dark:text-slate-200 dark:placeholder-slate-500 dark:focus:ring-blue-900"
placeholder="输入院校名称"
>
</div>
<button
class="h-9 rounded-r bg-blue-500 px-6 text-white transition-colors active:bg-blue-700 hover:bg-blue-600 focus:outline-none"
class="h-9 rounded-r bg-blue-500 px-6 text-white transition-colors hover:bg-blue-600 focus:outline-none active:bg-blue-700"
@click="handleSearch"
>
搜索
@ -224,7 +197,6 @@ onUnmounted(() => {
</div>
<!-- 下拉菜单面板 (绝对定位) -->
<!-- 使用 Transition 添加简单的淡入淡出效果 -->
<transition
enter-active-class="transition duration-100 ease-out"
enter-from-class="transform scale-95 opacity-0"
@ -233,25 +205,27 @@ onUnmounted(() => {
leave-from-class="transform scale-100 opacity-100"
leave-to-class="transform scale-95 opacity-0"
>
<!-- style="min-width: 600px;" -->
<!-- Update: 面板背景与边框 -->
<div
v-if="activeFilter"
class="absolute left-0 top-full z-50 mt-2 border border-gray-100 rounded-lg bg-white p-5 shadow-xl"
class="absolute left-0 top-full z-50 mt-2 border border-gray-100 bg-white p-5 shadow-xl rounded-lg dark:bg-slate-800 dark:border-slate-700"
>
<!-- 下拉内容区域 -->
<!-- 1. 位置 (Location) 内容 -->
<div v-if="activeFilter === 'location'">
<div class="flex items-start gap-4">
<span class="mt-1 shrink-0 text-gray-400 font-medium">院校所属</span>
<div class="xs:grid-cols-3 grid gap-2 lg:grid-cols-6 md:grid-cols-5 sm:grid-cols-3">
<!-- Update: 标签文字颜色 -->
<span class="mt-1 shrink-0 font-medium text-gray-400 dark:text-slate-500">院校所属</span>
<div class="grid gap-2 sm:grid-cols-3 md:grid-cols-5 lg:grid-cols-6 xs:grid-cols-3">
<!-- Update: 选项 Tag 的选中/未选中态 -->
<button
v-for="city in locations"
:key="city"
class="rounded px-3 py-1.5 text-center transition-colors hover:text-blue-500"
class="rounded px-3 py-1.5 text-center transition-colors hover:text-blue-500 dark:hover:text-blue-400"
:class="selectedFilters.location === city
? 'bg-blue-50 text-blue-500 font-medium'
: 'text-gray-600 bg-gray-50 hover:bg-gray-100'"
? 'bg-blue-50 text-blue-500 font-medium dark:bg-blue-900/40 dark:text-blue-400'
: 'text-gray-600 bg-gray-50 hover:bg-gray-100 dark:bg-slate-700 dark:text-slate-300 dark:hover:bg-slate-600'"
@click="selectOption('location', city)"
>
{{ city }}
@ -260,19 +234,21 @@ onUnmounted(() => {
</div>
</div>
<!-- 2. 其他筛选器占位内容 (类型专业排序) -->
<div v-else class="p-4 text-center text-gray-400">
<!-- 2. 其他筛选器占位内容 -->
<div v-else class="p-4 text-center text-gray-400 dark:text-slate-500">
这里是 {{ filters.find(f => f.key === activeFilter)?.label }} 的筛选选项
</div>
<!-- 底部操作按钮 -->
<div class="mt-6 flex items-center justify-between border-t border-gray-100 pt-4">
<div class="text-gray-500">
已选 <span class="text-blue-500 font-bold">{{ countSelected }}</span>
<!-- Update: 分割线颜色 -->
<div class="mt-6 flex items-center justify-between border-t border-gray-100 pt-4 dark:border-slate-700">
<div class="text-gray-500 dark:text-slate-400">
已选 <span class="font-bold text-blue-500 dark:text-blue-400">{{ countSelected }}</span>
</div>
<div class="flex gap-3">
<!-- Update: 清空按钮 Hover 背景 -->
<button
class="border border-blue-500 rounded px-4 py-1.5 text-blue-500 transition-colors hover:bg-blue-50"
class="rounded border border-blue-500 px-4 py-1.5 text-blue-500 transition-colors hover:bg-blue-50 dark:text-blue-400 dark:border-blue-500 dark:hover:bg-blue-900/20"
@click="clearCurrentFilter"
>
清空已选
@ -288,11 +264,4 @@ onUnmounted(() => {
</div>
</transition>
</div>
</template>
<style scoped>
/*
如果你的 tailwind 配置没有自动移除按钮的默认样式可能需要以下代码
一般在 tailwind base 中已经处理好了
*/
</style>
</template>

View File

@ -120,16 +120,14 @@ const overlayPositionClass = computed(() => {
// 2. ( Popover)
const arrowPositionClass = computed(() => {
const base = 'absolute h-3 w-3 bg-white border-slate-200 z-[-1]' // z-index -1
// Update: dark:bg-slate-800 dark:border-slate-700
const base = 'absolute h-3 w-3 bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700 z-[-1]'
//
// const centerOffset = props.arrowPointAtCenter ? 'left-1/2 -translate-x-1/2' : ''
// PointAtCenter Left/Right
const hAlign = props.arrowPointAtCenter ? '' : (props.placement.includes('left') ? 'left-4' : props.placement.includes('right') ? 'right-4' : '')
const vAlign = props.arrowPointAtCenter ? '' : (props.placement.includes('top') ? 'top-3' : props.placement.includes('bottom') ? 'bottom-3' : '')
// 12
// Top Bottom (45)
if (props.placement.startsWith('top')) {
return `${base} -bottom-1.5 border-b border-r rotate-45 ${props.placement === 'top' || props.arrowPointAtCenter ? 'left-1/2 -translate-x-1/2' : hAlign}`
}
@ -145,7 +143,7 @@ const arrowPositionClass = computed(() => {
return ''
})
// 3. (Transform Origin)
// 3.
const transitionOriginClass = computed(() => {
if (props.placement.startsWith('top'))
return 'origin-bottom'
@ -162,9 +160,13 @@ const transitionOriginClass = computed(() => {
const okBtnClass = computed(() => {
const base = 'rounded px-3 py-1 text-xs text-white transition-colors'
switch (props.okType) {
case 'danger': return `${base} bg-red-500 hover:bg-red-600 border border-red-500`
case 'default': return 'rounded px-3 py-1 text-xs text-slate-600 border border-slate-200 hover:border-blue-400 hover:text-blue-500 transition-colors bg-white'
default: return `${base} bg-blue-600 hover:bg-blue-700 border border-blue-600`
case 'danger':
return `${base} bg-red-500 hover:bg-red-600 border border-red-500`
case 'default':
// Update: /
return 'rounded px-3 py-1 text-xs text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-600 hover:border-blue-400 hover:text-blue-500 transition-colors bg-white dark:bg-slate-800'
default:
return `${base} bg-blue-600 hover:bg-blue-700 border border-blue-600`
}
})
</script>
@ -187,8 +189,13 @@ const okBtnClass = computed(() => {
>
<div
v-if="isVisible"
class="absolute z-50 w-45 cursor-default border border-slate-200 rounded-lg bg-white p-3 shadow-xl"
:class="[overlayPositionClass, transitionOriginClass]"
class="absolute z-50 w-45 cursor-default border rounded-lg p-3 shadow-xl"
:class="[
overlayPositionClass,
transitionOriginClass,
// Update: Dark Mode
'bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700'
]"
@click.stop
>
<!-- Arrow -->
@ -198,18 +205,20 @@ const okBtnClass = computed(() => {
<div class="relative z-10 flex items-start gap-3">
<div class="mt-0.5 flex-shrink-0">
<slot name="icon">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-orange-500" viewBox="0 0 20 20" fill="currentColor">
<!-- Update: Icon 颜色微调适配暗色 -->
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-orange-500 dark:text-orange-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</slot>
</div>
<div class="flex-1">
<div class="break-words text-sm text-slate-800 font-bold">
<!-- Update: 文字颜色适配 -->
<div class="break-words text-sm font-bold text-slate-800 dark:text-slate-100">
<slot name="title">
{{ title }}
</slot>
</div>
<div v-if="description || $slots.description" class="mt-1 break-words text-xs text-slate-500">
<div v-if="description || $slots.description" class="mt-1 break-words text-xs text-slate-500 dark:text-slate-400">
<slot name="description">
{{ description }}
</slot>
@ -222,7 +231,12 @@ const okBtnClass = computed(() => {
<slot name="cancelButton">
<button
v-if="showCancel"
class="rounded px-2 py-1 text-xs text-slate-500 transition-colors hover:bg-slate-100"
class="rounded px-2 py-1 text-xs transition-colors"
:class="[
// Update:
'text-slate-500 dark:text-slate-400',
'hover:bg-slate-100 dark:hover:bg-slate-700'
]"
:disabled="cancelButtonProps.disabled"
@click="handleCancel"
>
@ -248,4 +262,4 @@ const okBtnClass = computed(() => {
</div>
</Transition>
</div>
</template>
</template>

View File

@ -801,7 +801,7 @@ function deletePlan(planId: string) {
<!-- 注意bg-white 是为了遮挡滚动的文字group-hover:bg-slate-50 是为了保持 hover 效果 -->
<td
rowspan="4"
class="sticky left-0 z-20 border-r border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 p-4 align-top shadow-[4px_0_8px_-4px_rgba(0,0,0,0.1)] group-hover:bg-slate-50 dark:group-hover:bg-slate-800/50"
class="sticky left-0 z-20 border-r border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 p-4 align-top shadow-[4px_0_8px_-4px_rgba(0,0,0,0.1)] group-hover:bg-slate-50 dark:group-hover:bg-slate-800"
>
<div class="mb-1 w-52 truncate text-left text-base text-slate-900 dark:text-slate-100 font-bold" :title="school.schoolName">
{{ school.schoolName }}
@ -831,7 +831,7 @@ function deletePlan(planId: string) {
<td
rowspan="4"
class="border-r border-slate-100 bg-white p-2 text-center align-top group-hover:bg-slate-50"
class="border-r border-slate-100 dark:border-slate-800 bg-white dark:bg-slate-900 p-2 text-center align-top group-hover:bg-slate-50 dark:group-hover:bg-slate-800"
>
<div class="mb-2 text-lg font-bold text-slate-900 dark:text-slate-100">
{{ school.enrollProbability }}%
@ -851,7 +851,7 @@ function deletePlan(planId: string) {
<td
rowspan="4"
class="border-r border-slate-100 bg-white p-2 text-center align-top group-hover:bg-slate-50"
class="border-r border-slate-100 dark:border-slate-800 bg-white dark:bg-slate-900 p-2 text-center align-top group-hover:bg-slate-50 dark:group-hover:bg-slate-800"
>
<div class="text-lg font-medium text-slate-900 dark:text-slate-100">
{{ school.planNum }}
@ -870,7 +870,7 @@ function deletePlan(planId: string) {
<!-- Sticky Right: 操作 -->
<td
rowspan="4"
class="sticky right-0 z-20 border-l border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 p-4 text-center align-middle shadow-[-4px_0_8px_-4px_rgba(0,0,0,0.1)] group-hover:bg-slate-50 dark:group-hover:bg-slate-800/50"
class="sticky right-0 z-20 border-l border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 p-4 text-center align-middle shadow-[-4px_0_8px_-4px_rgba(0,0,0,0.1)] group-hover:bg-slate-50 dark:group-hover:bg-slate-800"
>
<button
class="whitespace-nowrap border border-blue-500 rounded-full bg-white dark:bg-slate-900 px-3 py-1.5 text-xs text-blue-500 dark:text-blue-400 transition-colors hover:bg-blue-50 dark:hover:bg-blue-900/20"
@ -1272,7 +1272,7 @@ function deletePlan(planId: string) {
省内招生人数
</th>
<th
class="sticky right-0 bg-slate-100 px-6 py-3 text-center shadow-[-4px_0_8px_-4px_rgba(0,0,0,0.1)]"
class="sticky right-0 px-6 py-3 text-center shadow-[-4px_0_8px_-4px_rgba(0,0,0,0.1)]"
>
加入志愿
</th>
@ -1325,7 +1325,7 @@ function deletePlan(planId: string) {
<!-- 操作列加入/移除 -->
<td
class="sticky right-0 bg-white dark:bg-slate-900 px-6 py-4 text-center shadow-[-4px_0_8px_-4px_rgba(0,0,0,0.1)] group-hover:bg-blue-50/30 dark:group-hover:bg-blue-900/10"
class="sticky right-0 border-l border-slate-100 dark:border-slate-800 bg-white dark:bg-slate-900 px-6 py-4 text-center shadow-[-4px_0_8px_-4px_rgba(0,0,0,0.1)] group-hover:bg-blue-50 dark:group-hover:bg-slate-800"
>
<button
class="min-w-[90px] rounded-full px-3 py-1.5 text-xs font-medium transition-all duration-200"