优化: 知识卡片交互+deepseek模型+鼠标钩子

This commit is contained in:
2026-06-05 21:45:45 +08:00
parent 004dba7db6
commit 0302546267
10 changed files with 667 additions and 77 deletions

View File

@@ -3,18 +3,73 @@
<div class="knowledge-header">
<span class="knowledge-label">知识卡片</span>
<span class="knowledge-keyword-tag" v-if="state.knowledge.keyword">#{{ state.knowledge.keyword }}</span>
<button class="knowledge-refresh-btn" @click="refresh" title="换一条"></button>
</div>
<div class="knowledge-divider"></div>
<div class="knowledge-body" v-if="state.knowledge.content">
<span class="knowledge-quote">"</span>
<span class="knowledge-text">{{ state.knowledge.content }}</span>
<div class="knowledge-text" v-html="renderedContent"></div>
</div>
<div class="knowledge-placeholder" v-else>请在设置中配置知识关键字</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted } from 'vue'
import { state } from '../composables/useOverlayState'
function refresh() {
state.knowledge = { ...state.knowledge, content: '加载中...' }
;(window as any).refreshKnowledge?.()
}
function updateHitRect() {
const btn = document.querySelector('.knowledge-refresh-btn') as HTMLElement
if (!btn) return
const r = btn.getBoundingClientRect()
const dpr = window.devicePixelRatio || 1
// 扩大点击区域 8px 方便点击
const pad = 8 * dpr
;(window as any).setInteractiveRect?.(
Math.round(r.left * dpr - pad), Math.round(r.top * dpr - pad),
Math.round(r.right * dpr + pad), Math.round(r.bottom * dpr + pad)
)
}
onMounted(() => {
updateHitRect()
window.addEventListener('resize', updateHitRect)
})
onUnmounted(() => {
window.removeEventListener('resize', updateHitRect)
})
function renderMarkdown(text: string): string {
let html = text
// 代码块 (```...```) → <pre><code>
html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, '<pre><code>$2</code></pre>')
// 行内代码 (`...`) → <code>
html = html.replace(/`([^`]+)`/g, '<code>$1</code>')
// 加粗 (**...**) → <strong>
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
// 斜体 (*...*) → <em>
html = html.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<em>$1</em>')
// ## 标题 → <h3>
html = html.replace(/^##\s+(.+)$/gm, '<h3>$1</h3>')
// 无序列表 → <li>
html = html.replace(/^[-*]\s+(.+)$/gm, '<li>$1</li>')
// 连续 <li> 包裹 <ul>
html = html.replace(/((?:<li>[\s\S]*?<\/li>\s*)+)/g, '<ul>$1</ul>')
// 段落(双换行分隔)
html = html.replace(/\n\n+/g, '</p><p>')
// 单换行 → <br>(非 <li>、<h3>、<pre> 内部)
html = html.replace(/(?<!<\/li>|<\/h3>|<\/pre>|<\/code>)\n(?!<li>|<h3>|<pre>|<\/ul>)/g, '<br>')
// 包裹在 <p> 中
if (!html.startsWith('<')) html = '<p>' + html + '</p>'
return html
}
const renderedContent = computed(() => renderMarkdown(state.knowledge.content))
</script>
<style scoped>
@@ -38,6 +93,25 @@ import { state } from '../composables/useOverlayState'
font-size: 10px; font-weight: 600;
border: 1px solid rgba(255,216,107,0.15);
}
.knowledge-refresh-btn {
margin-left: auto;
background: none; border: none; cursor: pointer;
color: var(--accent-warm); font-size: 16px; line-height: 1;
padding: 2px 4px; border-radius: 4px;
transition: all 0.3s;
opacity: 0.7;
animation: pulse-hint 3s ease-in-out infinite;
}
.knowledge-refresh-btn:hover,
.knowledge-refresh-btn.hover-active {
opacity: 1;
transform: rotate(180deg) scale(1.2);
background: rgba(255,216,107,0.15);
}
@keyframes pulse-hint {
0%, 100% { opacity: 0.5; }
50% { opacity: 0.8; }
}
.knowledge-divider {
height: 1px;
background: linear-gradient(90deg, var(--accent-warm), rgba(255,216,107,0.08), transparent);
@@ -56,9 +130,41 @@ import { state } from '../composables/useOverlayState'
.knowledge-text {
font-size: 15px; color: var(--text-main);
line-height: 1.7; text-shadow: 0 1px 4px rgba(0,0,0,0.5);
display: -webkit-box; -webkit-line-clamp: 8;
display: -webkit-box; -webkit-line-clamp: 10;
-webkit-box-orient: vertical; overflow: hidden;
}
.knowledge-text :deep(h3) {
font-size: 14px; font-weight: 700;
color: var(--accent-warm); margin: 10px 0 6px;
}
.knowledge-text :deep(strong) {
color: var(--text-strong); font-weight: 600;
}
.knowledge-text :deep(code) {
background: rgba(255,255,255,0.08);
padding: 1px 5px; border-radius: 3px;
font-size: 13px; font-family: 'Cascadia Code', 'Fira Code', monospace;
color: var(--accent-warm);
}
.knowledge-text :deep(pre) {
background: rgba(0,0,0,0.3);
padding: 8px 10px; border-radius: 6px;
margin: 8px 0; overflow-x: auto;
}
.knowledge-text :deep(pre code) {
background: none; padding: 0;
font-size: 13px;
}
.knowledge-text :deep(ul) {
padding-left: 16px; margin: 6px 0;
}
.knowledge-text :deep(li) {
font-size: 14px; line-height: 1.6;
list-style: disc;
}
.knowledge-text :deep(em) {
color: var(--text-faint); font-style: italic;
}
.knowledge-placeholder {
font-size: 13px; color: var(--text-faint);
font-style: italic; padding-top: 8px;

View File

@@ -48,6 +48,7 @@ import PhotoCard from './PhotoCard.vue'
pointer-events: none;
}
#layout-multi > .card { pointer-events: none; }
#card-knowledge { pointer-events: auto; }
#card-time {
grid-area: time; text-align: left;
width: 100%; height: 100%; padding: 22px 30px;

View File

@@ -38,6 +38,16 @@ const s = reactive<SettingsData>({
audioVizStyle: 'bars', audioSensitivity: 1.5, audioSmoothing: 0.7, audioColorScheme: 'neon',
})
async function refreshFromGo() {
const raw = await go.loadAllSettings()
const data = JSON.parse(raw) as SettingsData
if (data.lightTheme) document.documentElement.className = 'light'
Object.assign(s, { ...data, citiesByProv: data.citiesByProv || {}, provinces: data.provinces || [], themes: data.themes || [], savedColors: data.savedColors || [] })
if (data.color1) { currentColor.c1 = data.color1; currentColor.c2 = data.color2 || ''; currentColor.gradient = data.colorGradient || false }
}
;(window as any).__refreshSettings = refreshFromGo
// Shared state for color section
const currentColor = reactive({ c1: '', c2: '', gradient: false })
@@ -76,11 +86,7 @@ async function onRemoveSavedColor(idx: number) {
}
onMounted(async () => {
const raw = await go.loadAllSettings()
const data = JSON.parse(raw) as SettingsData
if (data.lightTheme) document.documentElement.className = 'light'
Object.assign(s, { ...data, citiesByProv: data.citiesByProv || {}, provinces: data.provinces || [], themes: data.themes || [], savedColors: data.savedColors || [] })
if (data.color1) { currentColor.c1 = data.color1; currentColor.c2 = data.color2 || ''; currentColor.gradient = data.colorGradient || false }
await refreshFromGo()
setTimeout(() => {
const el = document.documentElement
if (go.resizeToFit) go.resizeToFit(el.scrollWidth, el.scrollHeight + 8)