mirror of
https://github.com/stardrophere/InsightRadar.git
synced 2026-06-05 23:56:36 +08:00
搜索功能加入
This commit is contained in:
@@ -0,0 +1,704 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import VueApexCharts from 'vue3-apexcharts'
|
||||
import { searchEventsTimeline } from '@/api/events'
|
||||
import type { SearchTimelineResponse } from '@/types/event'
|
||||
import UnifiedEventCard from '@/components/UnifiedEventCard.vue'
|
||||
|
||||
const keyword = ref('')
|
||||
const searchResult = ref<SearchTimelineResponse | null>(null)
|
||||
const loading = ref(false)
|
||||
const hours = ref(2)
|
||||
const searchMode = ref<'exact' | 'semantic' | 'hybrid'>('hybrid')
|
||||
const selectedTimeLabel = ref<string | null>(null)
|
||||
|
||||
const filteredEvents = computed(() => {
|
||||
if (!searchResult.value) return []
|
||||
if (!selectedTimeLabel.value) return searchResult.value.events
|
||||
|
||||
const selectedPoint = searchResult.value.timeline.find(p => p.time_label === selectedTimeLabel.value)
|
||||
if (!selectedPoint) return []
|
||||
|
||||
const pointEventIds = selectedPoint.event_ids ?? []
|
||||
if (pointEventIds.length > 0) {
|
||||
const relatedIds = new Set(pointEventIds)
|
||||
return searchResult.value.events.filter(event => relatedIds.has(event.event_id))
|
||||
}
|
||||
|
||||
// 向后兼容:旧版后端未返回 event_ids 时,退化为时间范围过滤。
|
||||
return searchResult.value.events.filter(event => {
|
||||
const createdStr = event.created_at.replace('T', ' ')
|
||||
const activeStr = event.last_active_at.replace('T', ' ')
|
||||
const len = selectedTimeLabel.value!.length
|
||||
const start = createdStr.substring(0, len)
|
||||
const end = activeStr.substring(0, len)
|
||||
const target = selectedTimeLabel.value!
|
||||
return (start <= target && target <= end) || createdStr.startsWith(target) || activeStr.startsWith(target)
|
||||
})
|
||||
})
|
||||
|
||||
// 热度时间线图表配置。
|
||||
const chartOptions = ref({
|
||||
chart: {
|
||||
type: 'area',
|
||||
height: 350,
|
||||
background: 'transparent',
|
||||
toolbar: {
|
||||
show: true
|
||||
},
|
||||
animations: {
|
||||
enabled: true,
|
||||
easing: 'easeinout',
|
||||
speed: 800,
|
||||
},
|
||||
events: {
|
||||
markerClick: function(event: any, chartContext: any, { dataPointIndex }: any) {
|
||||
if (searchResult.value && searchResult.value.timeline[dataPointIndex]) {
|
||||
const clickedTime = searchResult.value.timeline[dataPointIndex].time_label
|
||||
if (selectedTimeLabel.value === clickedTime) {
|
||||
selectedTimeLabel.value = null
|
||||
} else {
|
||||
selectedTimeLabel.value = clickedTime
|
||||
// 点击时间点后平滑滚动到事件列表。
|
||||
setTimeout(() => {
|
||||
document.getElementById('events-section-anchor')?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
theme: {
|
||||
mode: 'dark' // 默认使用暗色图表主题,保证深浅主题下对比度都清晰。
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
stroke: {
|
||||
curve: 'smooth',
|
||||
width: 3
|
||||
},
|
||||
xaxis: {
|
||||
type: 'category',
|
||||
categories: [] as string[],
|
||||
labels: {
|
||||
style: {
|
||||
colors: '#9ca3af'
|
||||
}
|
||||
},
|
||||
axisBorder: {
|
||||
show: false
|
||||
},
|
||||
axisTicks: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
title: {
|
||||
text: '热点数量',
|
||||
style: {
|
||||
color: '#9ca3af',
|
||||
fontWeight: 500
|
||||
}
|
||||
},
|
||||
labels: {
|
||||
style: {
|
||||
colors: '#9ca3af'
|
||||
}
|
||||
}
|
||||
},
|
||||
fill: {
|
||||
type: 'gradient',
|
||||
gradient: {
|
||||
shadeIntensity: 1,
|
||||
opacityFrom: 0.6,
|
||||
opacityTo: 0.1,
|
||||
stops: [0, 90, 100]
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
borderColor: 'rgba(156, 163, 175, 0.1)',
|
||||
strokeDashArray: 4,
|
||||
},
|
||||
colors: ['#6366f1'],
|
||||
tooltip: {
|
||||
theme: 'dark'
|
||||
}
|
||||
})
|
||||
|
||||
const series = ref([{
|
||||
name: '热点分布',
|
||||
data: [] as number[]
|
||||
}])
|
||||
|
||||
async function handleSearch() {
|
||||
if (!keyword.value.trim()) return
|
||||
|
||||
loading.value = true
|
||||
searchResult.value = null
|
||||
selectedTimeLabel.value = null
|
||||
try {
|
||||
const res = await searchEventsTimeline(keyword.value.trim(), hours.value, searchMode.value)
|
||||
searchResult.value = res
|
||||
|
||||
// 查询成功后同步刷新图表横轴与序列。
|
||||
chartOptions.value = {
|
||||
...chartOptions.value,
|
||||
xaxis: {
|
||||
...chartOptions.value.xaxis,
|
||||
categories: res.timeline.map(p => p.time_label)
|
||||
}
|
||||
}
|
||||
series.value = [{
|
||||
name: '热点数量',
|
||||
data: res.timeline.map(p => p.count)
|
||||
}]
|
||||
} catch (error) {
|
||||
console.error('搜索失败', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="search-view">
|
||||
<div class="content-wrapper">
|
||||
<header class="page-header">
|
||||
<div class="header-title-row">
|
||||
<h1 class="page-title">
|
||||
<i class="fa-solid fa-chart-line" style="color: var(--brand-primary)"></i>
|
||||
事件追踪分析
|
||||
</h1>
|
||||
</div>
|
||||
<p class="page-desc">基于语义匹配与正则检索,分析关键词在时间维度上的热度变化与关联聚合事件。</p>
|
||||
</header>
|
||||
|
||||
<div class="top-panels">
|
||||
<div class="search-box glass-panel">
|
||||
<h2 class="panel-title"><i class="fa-solid fa-magnifying-glass-chart"></i> 关键词搜索</h2>
|
||||
<div class="search-controls">
|
||||
<div class="input-wrapper">
|
||||
<i class="fa-solid fa-magnifying-glass search-icon"></i>
|
||||
<input
|
||||
v-model="keyword"
|
||||
@keyup.enter="handleSearch"
|
||||
type="text"
|
||||
placeholder="请输入关键词"
|
||||
class="search-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="time-select-wrapper">
|
||||
<i class="fa-regular fa-clock select-icon"></i>
|
||||
<select v-model="hours" class="time-select">
|
||||
<option :value="2">最近 2 小时</option>
|
||||
<option :value="12">最近 12 小时</option>
|
||||
<option :value="24">最近 24 小时</option>
|
||||
<option :value="48">最近 48 小时</option>
|
||||
<option :value="168">最近 7 天</option>
|
||||
<option :value="360">最近 15 天</option>
|
||||
</select>
|
||||
<i class="fa-solid fa-chevron-down select-arrow"></i>
|
||||
</div>
|
||||
<div class="time-select-wrapper">
|
||||
<i class="fa-solid fa-filter select-icon"></i>
|
||||
<select v-model="searchMode" class="time-select">
|
||||
<option value="hybrid">混合匹配</option>
|
||||
<option value="exact">关键词匹配</option>
|
||||
<option value="semantic">语义匹配</option>
|
||||
</select>
|
||||
<i class="fa-solid fa-chevron-down select-arrow"></i>
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="handleSearch" :disabled="loading || !keyword.trim()">
|
||||
<i class="fa-solid fa-bolt" v-if="!loading"></i>
|
||||
<i class="fa-solid fa-spinner fa-spin" v-else></i>
|
||||
追踪计算
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tips-box glass-panel">
|
||||
<h2 class="panel-title"><i class="fa-regular fa-lightbulb"></i> 搜索建议</h2>
|
||||
<div class="tips-content">
|
||||
<button class="tip-tag" @click="keyword='新能源汽车'; hours=168; handleSearch()">
|
||||
<i class="fa-solid fa-rocket"></i> 新能源汽车
|
||||
</button>
|
||||
<button class="tip-tag" @click="keyword='苹果公司'; hours=168; handleSearch()">
|
||||
<i class="fa-brands fa-apple"></i> 苹果产业链
|
||||
</button>
|
||||
<button class="tip-tag regex-tag" @click="keyword='AI|LLM'; hours=168; handleSearch()">
|
||||
<i class="fa-solid fa-code-branch"></i> AI / 大模型
|
||||
</button>
|
||||
<button class="tip-tag regex-tag" @click="keyword='美国关税'; hours=168; handleSearch()">
|
||||
<i class="fa-solid fa-flag-usa"></i> 美国关税
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading-state glass-panel">
|
||||
<div class="spinner-wrapper">
|
||||
<i class="fa-solid fa-circle-notch fa-spin"></i>
|
||||
</div>
|
||||
<p>引擎正在计算热度模型并提取时间节点,请稍候...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="searchResult" class="results-container">
|
||||
<section class="chart-section glass-panel">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">
|
||||
<i class="fa-solid fa-wave-square"></i> 时间热度脉络
|
||||
</h2>
|
||||
<span class="meta-info">共 {{ searchResult.timeline.length }} 个时间节点 · 覆盖 {{ searchResult.events.length }} 个聚合事件</span>
|
||||
</div>
|
||||
|
||||
<div class="chart-container" v-if="searchResult.timeline.length > 0">
|
||||
<VueApexCharts
|
||||
type="area"
|
||||
height="350"
|
||||
:options="chartOptions"
|
||||
:series="series"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
<i class="fa-regular fa-folder-open"></i>
|
||||
<p>该时间范围暂无热度节点</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="events-section-anchor" class="events-section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">
|
||||
<i class="fa-solid fa-layer-group"></i> 关联深度聚合事件
|
||||
<span v-if="selectedTimeLabel" class="time-filter-badge">
|
||||
筛选: {{ selectedTimeLabel }}
|
||||
<i class="fa-solid fa-xmark" @click="selectedTimeLabel = null"></i>
|
||||
</span>
|
||||
</h2>
|
||||
<span class="meta-info">共检索到 {{ filteredEvents.length }} 个聚合事件</span>
|
||||
</div>
|
||||
<div class="events-grid" v-if="filteredEvents.length > 0">
|
||||
<UnifiedEventCard
|
||||
v-for="event in filteredEvents"
|
||||
:key="event.event_id"
|
||||
:event="event"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="empty-state glass-panel">
|
||||
<i class="fa-solid fa-satellite-dish"></i>
|
||||
<p v-if="selectedTimeLabel">该时间点未找到匹配事件,请尝试点击其他节点</p>
|
||||
<p v-else>暂无关联聚合事件,可尝试扩大时间范围或更换关键词</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-view {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.header-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 毛玻璃面板 */
|
||||
.glass-panel {
|
||||
background: var(--bg-surface);
|
||||
backdrop-filter: var(--backdrop-blur);
|
||||
-webkit-backdrop-filter: var(--backdrop-blur);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow: var(--shadow-sm);
|
||||
padding: 24px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.glass-panel:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
border-color: rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
.top-panels {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
flex: 2;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tips-box {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin: 0 0 20px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.panel-title i {
|
||||
color: var(--brand-primary);
|
||||
}
|
||||
|
||||
.tips-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tip-tag {
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border-subtle);
|
||||
color: var(--text-secondary);
|
||||
padding: 8px 14px;
|
||||
border-radius: var(--radius-lg);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tip-tag i {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.tip-tag:hover {
|
||||
background: var(--brand-primary-alpha);
|
||||
color: var(--brand-primary);
|
||||
border-color: var(--brand-primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.15);
|
||||
}
|
||||
|
||||
.tip-tag.regex-tag {
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.search-controls {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
color: var(--text-placeholder);
|
||||
font-size: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 14px 16px 14px 44px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
background-color: var(--bg-input);
|
||||
color: var(--text-primary);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--brand-primary);
|
||||
box-shadow: 0 0 0 3px var(--brand-primary-alpha);
|
||||
background-color: var(--bg-surface);
|
||||
}
|
||||
|
||||
.time-select-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.select-icon {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
color: var(--text-placeholder);
|
||||
font-size: 15px;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.select-arrow {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
color: var(--text-placeholder);
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.time-select {
|
||||
width: 100%;
|
||||
padding: 14px 36px 14px 40px;
|
||||
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;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.time-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--brand-primary);
|
||||
box-shadow: 0 0 0 3px var(--brand-primary-alpha);
|
||||
background-color: var(--bg-surface);
|
||||
}
|
||||
|
||||
.time-select-wrapper:hover .time-select {
|
||||
border-color: var(--brand-primary);
|
||||
}
|
||||
|
||||
.time-select-wrapper:hover .select-icon,
|
||||
.time-select-wrapper:hover .select-arrow {
|
||||
color: var(--brand-primary);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 14px 28px;
|
||||
background-color: var(--brand-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-lg);
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background-color: var(--brand-primary-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 16px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:active:not(:disabled) {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.results-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.section-title i {
|
||||
color: var(--brand-primary);
|
||||
}
|
||||
|
||||
.time-filter-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-left: 12px;
|
||||
padding: 4px 10px;
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
color: var(--brand-primary);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.time-filter-badge i {
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: var(--brand-primary) !important;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.time-filter-badge i:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.meta-info {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chart-section {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
margin-top: 16px;
|
||||
margin-left: -10px; /* 视觉上抵消 apexcharts 的默认左侧留白。 */
|
||||
}
|
||||
|
||||
.events-section {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.events-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* 与 DashboardView 保持一致,列表按纵向堆叠展示。 */
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--text-secondary);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.spinner-wrapper {
|
||||
font-size: 32px;
|
||||
color: var(--brand-primary);
|
||||
}
|
||||
|
||||
.loading-state p {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 36px;
|
||||
opacity: 0.4;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.top-panels {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.search-controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user