mirror of
https://github.com/stardrophere/InsightRadar.git
synced 2026-06-05 21:57:50 +08:00
807 lines
19 KiB
Vue
807 lines
19 KiB
Vue
<script setup lang="ts">
|
||
import { onMounted, ref, computed } from 'vue'
|
||
|
||
import { useAuthStore } from '@/stores/auth'
|
||
import { fetchPreferences, createPreference, deletePreference, fetchRecommendedEvents } from '@/api/preferences'
|
||
import type { UserTopicPreference, MatchedEvent } from '@/types/preference'
|
||
|
||
const authStore = useAuthStore()
|
||
const userId = computed(() => authStore.user?.id ?? 0)
|
||
|
||
const preferences = ref<UserTopicPreference[]>([])
|
||
const newKeyword = ref('')
|
||
const loading = ref(true)
|
||
const submitting = ref(false)
|
||
const error = ref('')
|
||
const successMsg = ref('')
|
||
|
||
const matchedEvents = ref<MatchedEvent[]>([])
|
||
const loadingMatched = ref(false)
|
||
const matchedError = ref('')
|
||
|
||
const hoursRange = ref(72)
|
||
const sortBy = ref('match_score')
|
||
|
||
const hoursOptions = [
|
||
{ label: '24小时', value: 24 },
|
||
{ label: '72小时', value: 72 },
|
||
{ label: '7天', value: 168 },
|
||
]
|
||
|
||
const sortOptions = [
|
||
{ label: '匹配度', value: 'match_score' },
|
||
{ label: '最新', value: 'created_at' },
|
||
]
|
||
|
||
function onHoursChange(value: number) {
|
||
hoursRange.value = value
|
||
loadMatchedEvents()
|
||
}
|
||
|
||
function onSortChange(value: string) {
|
||
sortBy.value = value
|
||
loadMatchedEvents()
|
||
}
|
||
|
||
/** 加载用户的兴趣关键词 */
|
||
async function loadPreferences() {
|
||
if (!userId.value) return
|
||
loading.value = true
|
||
error.value = ''
|
||
try {
|
||
preferences.value = await fetchPreferences(userId.value)
|
||
} catch (e) {
|
||
error.value = e instanceof Error ? e.message : '加载失败'
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
/** 加载命中关键词的推荐事件 */
|
||
async function loadMatchedEvents() {
|
||
if (!userId.value) return
|
||
loadingMatched.value = true
|
||
matchedError.value = ''
|
||
try {
|
||
const result = await fetchRecommendedEvents(userId.value, {
|
||
limit: 30,
|
||
hours: hoursRange.value,
|
||
sort_by: sortBy.value
|
||
})
|
||
matchedEvents.value = result.data
|
||
} catch (e) {
|
||
matchedError.value = e instanceof Error ? e.message : '加载失败'
|
||
} finally {
|
||
loadingMatched.value = false
|
||
}
|
||
}
|
||
|
||
/** 格式化热度标签 */
|
||
function hotLabel(score: number): { text: string; color: string; bg: string } {
|
||
if (score >= 50) return { text: `🔥 ${score}`, color: '#ef4444', bg: 'rgba(239,68,68,0.1)' }
|
||
if (score >= 20) return { text: `🌡 ${score}`, color: '#f59e0b', bg: 'rgba(245,158,11,0.1)' }
|
||
return { text: `${score}`, color: 'var(--text-secondary)', bg: 'var(--bg-input)' }
|
||
}
|
||
|
||
/** 添加新关键词 */
|
||
async function handleAdd() {
|
||
const keyword = newKeyword.value.trim()
|
||
if (!keyword) return
|
||
if (!userId.value) return
|
||
|
||
submitting.value = true
|
||
error.value = ''
|
||
successMsg.value = ''
|
||
try {
|
||
const created = await createPreference(userId.value, keyword)
|
||
preferences.value.unshift(created)
|
||
newKeyword.value = ''
|
||
successMsg.value = `已添加「${keyword}」`
|
||
setTimeout(() => { successMsg.value = '' }, 3000)
|
||
loadMatchedEvents()
|
||
} catch (e) {
|
||
error.value = e instanceof Error ? e.message : '添加失败'
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
/** 删除关键词 */
|
||
async function handleDelete(pref: UserTopicPreference) {
|
||
if (!userId.value) return
|
||
error.value = ''
|
||
try {
|
||
await deletePreference(userId.value, pref.id)
|
||
preferences.value = preferences.value.filter(p => p.id !== pref.id)
|
||
loadMatchedEvents()
|
||
} catch (e) {
|
||
error.value = e instanceof Error ? e.message : '删除失败'
|
||
}
|
||
}
|
||
|
||
/** Enter 键提交 */
|
||
function onInputKeydown(e: KeyboardEvent) {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault()
|
||
handleAdd()
|
||
}
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await loadPreferences()
|
||
loadMatchedEvents()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="topics-page">
|
||
<div class="page-header">
|
||
<h1>
|
||
<i class="fa-solid fa-rss" style="color: var(--brand-primary)"></i>
|
||
我的泛订阅
|
||
</h1>
|
||
<p class="page-desc">
|
||
添加你感兴趣的关键词,系统会自动匹配全网热点事件并推送给你。
|
||
支持精确匹配和 AI 语义匹配。
|
||
</p>
|
||
</div>
|
||
|
||
<!-- 添加关键词 -->
|
||
<div class="add-section">
|
||
<div class="add-form">
|
||
<div class="input-wrapper">
|
||
<i class="fa-solid fa-plus input-icon"></i>
|
||
<input
|
||
v-model="newKeyword"
|
||
type="text"
|
||
class="keyword-input"
|
||
placeholder="输入关键词,如「篮球」「科比」「科技」..."
|
||
maxlength="100"
|
||
@keydown="onInputKeydown"
|
||
/>
|
||
</div>
|
||
<button class="add-btn" :disabled="submitting || !newKeyword.trim()" @click="handleAdd">
|
||
{{ submitting ? '添加中...' : '添加' }}
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 提示消息 -->
|
||
<p v-if="successMsg" class="msg success-msg">
|
||
<i class="fa-solid fa-check-circle"></i> {{ successMsg }}
|
||
</p>
|
||
<p v-if="error" class="msg error-msg">
|
||
<i class="fa-solid fa-circle-exclamation"></i> {{ error }}
|
||
</p>
|
||
</div>
|
||
|
||
<!-- 加载状态 -->
|
||
<div v-if="loading" class="loading-state">
|
||
<i class="fa-solid fa-spinner fa-spin"></i>
|
||
<span>加载中...</span>
|
||
</div>
|
||
|
||
<!-- 关键词列表 -->
|
||
<div v-else class="keywords-section">
|
||
<h2 class="sub-title">
|
||
已订阅的关键词
|
||
<span class="count-badge">{{ preferences.length }}</span>
|
||
</h2>
|
||
|
||
<div v-if="preferences.length === 0" class="empty-state">
|
||
<i class="fa-solid fa-bookmark"></i>
|
||
<p>还没有添加任何关键词</p>
|
||
<p class="empty-hint">在上方输入框中添加你感兴趣的话题</p>
|
||
</div>
|
||
|
||
<div v-else class="keywords-grid">
|
||
<div v-for="pref in preferences" :key="pref.id" class="keyword-card">
|
||
<div class="keyword-content">
|
||
<i class="fa-solid fa-hashtag keyword-icon"></i>
|
||
<span class="keyword-text">{{ pref.interested_keyword }}</span>
|
||
</div>
|
||
<button class="delete-btn" title="删除" @click="handleDelete(pref)">
|
||
<i class="fa-solid fa-xmark"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 命中的热点事件 -->
|
||
<div class="matched-section">
|
||
<div class="section-header">
|
||
<h2 class="sub-title">
|
||
<i class="fa-solid fa-wand-magic-sparkles" style="color: var(--brand-primary)"></i>
|
||
命中的热点事件
|
||
<span v-if="!loadingMatched && matchedEvents.length > 0" class="count-badge">
|
||
{{ matchedEvents.length }}
|
||
</span>
|
||
</h2>
|
||
|
||
<!-- 筛选排序栏 -->
|
||
<div v-if="preferences.length > 0" class="filters-bar">
|
||
<div class="filter-group">
|
||
<span class="filter-label">
|
||
<i class="fa-regular fa-clock"></i> 时间范围
|
||
</span>
|
||
<div class="filter-tabs">
|
||
<button
|
||
v-for="opt in hoursOptions"
|
||
:key="opt.value"
|
||
class="filter-tab"
|
||
:class="{ active: hoursRange === opt.value }"
|
||
@click="onHoursChange(opt.value)"
|
||
>
|
||
{{ opt.label }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="filter-group">
|
||
<span class="filter-label">
|
||
<i class="fa-solid fa-arrow-down-wide-short"></i> 排序方式
|
||
</span>
|
||
<div class="filter-tabs">
|
||
<button
|
||
v-for="opt in sortOptions"
|
||
:key="opt.value"
|
||
class="filter-tab"
|
||
:class="{ active: sortBy === opt.value }"
|
||
@click="onSortChange(opt.value)"
|
||
>
|
||
{{ opt.label }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 加载中 -->
|
||
<div v-if="loadingMatched" class="loading-state">
|
||
<i class="fa-solid fa-spinner fa-spin"></i>
|
||
<span>AI 匹配中...</span>
|
||
</div>
|
||
|
||
<!-- 错误 -->
|
||
<p v-else-if="matchedError" class="msg error-msg">
|
||
<i class="fa-solid fa-circle-exclamation"></i> {{ matchedError }}
|
||
</p>
|
||
|
||
<!-- 无关键词 -->
|
||
<div v-else-if="preferences.length === 0" class="empty-state">
|
||
<i class="fa-solid fa-search"></i>
|
||
<p>先添加关键词才能查看匹配事件</p>
|
||
</div>
|
||
|
||
<!-- 无匹配 -->
|
||
<div v-else-if="matchedEvents.length === 0" class="empty-state">
|
||
<i class="fa-solid fa-satellite-dish"></i>
|
||
<p>暂未匹配到相关事件</p>
|
||
<p class="empty-hint">系统会在下次 AI 摘要生成后自动更新</p>
|
||
</div>
|
||
|
||
<!-- 匹配事件列表 -->
|
||
<div v-else class="matched-list">
|
||
<RouterLink
|
||
v-for="ev in matchedEvents"
|
||
:key="ev.event_id"
|
||
:to="{ path: '/', query: { event: ev.event_id } }"
|
||
class="matched-card"
|
||
>
|
||
<!-- 热度 + 匹配度 -->
|
||
<div class="matched-card-meta">
|
||
<span
|
||
class="hot-chip"
|
||
:style="{ color: hotLabel(ev.hot_score).color, background: hotLabel(ev.hot_score).bg }"
|
||
>
|
||
{{ hotLabel(ev.hot_score).text }}
|
||
</span>
|
||
<span class="match-score-chip">
|
||
<i class="fa-solid fa-crosshairs"></i>
|
||
匹配度 {{ ev.match_score.toFixed(0) }}
|
||
</span>
|
||
<span class="matched-goto">
|
||
<i class="fa-solid fa-arrow-up-right-from-square"></i> 查看详情
|
||
</span>
|
||
</div>
|
||
|
||
<!-- 标题 -->
|
||
<p class="matched-title">{{ ev.unified_title }}</p>
|
||
|
||
<!-- AI 摘要 -->
|
||
<p v-if="ev.summary" class="matched-summary">{{ ev.summary }}</p>
|
||
|
||
<!-- 命中的关键词标签 -->
|
||
<div class="matched-hits">
|
||
<span v-for="hit in ev.exact_hits.slice(0, 4)" :key="hit" class="hit-tag exact">
|
||
<i class="fa-solid fa-bullseye"></i> {{ hit }}
|
||
</span>
|
||
<span
|
||
v-for="sh in ev.semantic_hits.slice(0, 3)"
|
||
:key="sh.topic_keyword"
|
||
class="hit-tag semantic"
|
||
>
|
||
<i class="fa-solid fa-brain"></i> {{ sh.topic_keyword }}
|
||
<span class="sim-pct">{{ (sh.similarity * 100).toFixed(0) }}%</span>
|
||
</span>
|
||
</div>
|
||
</RouterLink>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 功能说明 -->
|
||
<div class="info-panel">
|
||
<h3><i class="fa-solid fa-lightbulb"></i> 匹配说明</h3>
|
||
<ul>
|
||
<li><strong>精确匹配:</strong>关键词与事件标签完全一致或互为包含关系时命中</li>
|
||
<li><strong>语义匹配:</strong>使用向量模型计算语义相似度,超过阈值自动命中</li>
|
||
<li><strong>推送触发:</strong>当新事件的标签命中您的关键词时,将在设定时间推送简报</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.topics-page {
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.page-header {
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.page-header h1 {
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
margin: 0 0 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.page-desc {
|
||
font-size: 14px;
|
||
color: var(--text-secondary);
|
||
line-height: 1.6;
|
||
margin: 0;
|
||
}
|
||
|
||
/* ==========================================
|
||
添加区域
|
||
========================================== */
|
||
.add-section {
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.add-form {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.input-wrapper {
|
||
flex: 1;
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.input-icon {
|
||
position: absolute;
|
||
left: 14px;
|
||
color: var(--text-placeholder);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.keyword-input {
|
||
width: 100%;
|
||
padding: 14px 16px 14px 42px;
|
||
background: var(--bg-input);
|
||
border: 1px solid var(--border-subtle);
|
||
border-radius: var(--radius-md);
|
||
color: var(--text-primary);
|
||
font-size: 15px;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
box-shadow: inset 0 2px 4px rgba(0,0,0,0.02);
|
||
}
|
||
|
||
.keyword-input::placeholder {
|
||
color: var(--text-placeholder);
|
||
}
|
||
|
||
.keyword-input:focus {
|
||
outline: none;
|
||
border-color: var(--brand-primary);
|
||
box-shadow: 0 0 0 4px var(--brand-primary-alpha), inset 0 1px 2px rgba(0,0,0,0.02);
|
||
background: var(--bg-surface);
|
||
}
|
||
|
||
.add-btn {
|
||
padding: 14px 28px;
|
||
background: linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-primary-hover) 100%);
|
||
color: #fff;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
border-radius: var(--radius-md);
|
||
white-space: nowrap;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
box-shadow: 0 4px 12px var(--brand-primary-alpha);
|
||
}
|
||
|
||
.add-btn:hover:not(:disabled) {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 20px var(--brand-primary-alpha);
|
||
filter: brightness(1.1);
|
||
}
|
||
|
||
.add-btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.msg {
|
||
font-size: 13px;
|
||
margin: 10px 0 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.success-msg {
|
||
color: var(--status-success);
|
||
}
|
||
|
||
.error-msg {
|
||
color: var(--status-error);
|
||
}
|
||
|
||
/* ==========================================
|
||
关键词列表
|
||
========================================== */
|
||
.keywords-section {
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.sub-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
margin: 0 0 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.count-badge {
|
||
font-size: 12px;
|
||
padding: 2px 10px;
|
||
border-radius: 10px;
|
||
background: var(--brand-primary-alpha);
|
||
color: var(--brand-primary);
|
||
font-weight: 700;
|
||
}
|
||
|
||
.loading-state {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
padding: 40px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 40px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.empty-state i {
|
||
font-size: 36px;
|
||
opacity: 0.3;
|
||
margin-bottom: 12px;
|
||
display: block;
|
||
}
|
||
|
||
.empty-state p {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.empty-hint {
|
||
margin-top: 6px !important;
|
||
font-size: 13px !important;
|
||
color: var(--text-placeholder);
|
||
}
|
||
|
||
.keywords-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
}
|
||
|
||
.keyword-card {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 16px;
|
||
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-lg);
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
|
||
.keyword-card:hover {
|
||
border-color: var(--brand-primary-alpha);
|
||
box-shadow: var(--shadow-md);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.keyword-content {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.keyword-icon {
|
||
color: var(--brand-primary);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.keyword-text {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.delete-btn {
|
||
color: var(--text-placeholder);
|
||
padding: 4px 6px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.delete-btn:hover {
|
||
color: var(--status-error);
|
||
background: rgba(239, 68, 68, 0.1);
|
||
}
|
||
|
||
/* ==========================================
|
||
命中事件区块
|
||
========================================== */
|
||
.matched-section {
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.section-header {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.section-header .sub-title {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.filters-bar {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16px;
|
||
padding: 12px 16px;
|
||
margin-bottom: 16px;
|
||
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-lg);
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
|
||
.filter-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.filter-label {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--text-secondary);
|
||
white-space: nowrap;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.filter-tabs {
|
||
display: flex;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
background: var(--bg-input);
|
||
padding: 4px;
|
||
border-radius: var(--radius-md);
|
||
border: 1px solid var(--border-subtle);
|
||
}
|
||
|
||
.filter-tab {
|
||
padding: 4px 12px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
border-radius: var(--radius-sm);
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--text-secondary);
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.filter-tab:hover {
|
||
color: var(--text-primary);
|
||
background: var(--bg-hover);
|
||
}
|
||
|
||
.filter-tab.active {
|
||
background: var(--bg-surface);
|
||
color: var(--brand-primary);
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
|
||
.matched-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.matched-card {
|
||
display: block;
|
||
padding: 16px;
|
||
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);
|
||
text-decoration: none;
|
||
color: inherit;
|
||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
.matched-card:hover {
|
||
border-color: var(--brand-primary-alpha);
|
||
box-shadow: var(--shadow-md);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.matched-card-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.hot-chip {
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
padding: 2px 8px;
|
||
border-radius: 99px;
|
||
}
|
||
|
||
.match-score-chip {
|
||
font-size: 11px;
|
||
color: var(--text-placeholder);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.matched-goto {
|
||
margin-left: auto;
|
||
font-size: 11px;
|
||
color: var(--brand-primary);
|
||
opacity: 0;
|
||
transition: opacity 0.2s;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.matched-card:hover .matched-goto {
|
||
opacity: 1;
|
||
}
|
||
|
||
.matched-title {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
line-height: 1.5;
|
||
margin: 0 0 6px;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.matched-summary {
|
||
font-size: 13px;
|
||
color: var(--text-secondary);
|
||
line-height: 1.6;
|
||
margin: 0 0 8px;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.matched-hits {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px;
|
||
}
|
||
|
||
.hit-tag {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 2px 8px;
|
||
border-radius: 99px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.hit-tag.exact {
|
||
background: rgba(99, 102, 241, 0.1);
|
||
color: #6366f1;
|
||
}
|
||
|
||
.hit-tag.semantic {
|
||
background: rgba(16, 185, 129, 0.1);
|
||
color: var(--status-success);
|
||
}
|
||
|
||
.sim-pct {
|
||
opacity: 0.7;
|
||
}
|
||
|
||
/* ==========================================
|
||
功能说明
|
||
========================================== */
|
||
.info-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);
|
||
padding: 24px;
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
|
||
.info-panel h3 {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
margin: 0 0 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.info-panel h3 i {
|
||
color: #facc15;
|
||
}
|
||
|
||
.info-panel ul {
|
||
list-style: none;
|
||
padding: 0;
|
||
margin: 0;
|
||
}
|
||
|
||
.info-panel li {
|
||
font-size: 13px;
|
||
color: var(--text-secondary);
|
||
line-height: 1.8;
|
||
padding-left: 16px;
|
||
position: relative;
|
||
}
|
||
|
||
.info-panel li::before {
|
||
content: '•';
|
||
position: absolute;
|
||
left: 0;
|
||
color: var(--brand-primary);
|
||
}
|
||
</style>
|