feat(desktop): ✨ 实现一些功能
1. 实现了用户阅读画像 2. 实现了全局检索功能
This commit is contained in:
19
src/renderer/src/hooks/useLoading.ts
Normal file
19
src/renderer/src/hooks/useLoading.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
/**
|
||||
* 加载状态Hook
|
||||
*/
|
||||
export default function useLoading(initValue = false) {
|
||||
const loading = ref(initValue);
|
||||
const setLoading = (value: boolean) => {
|
||||
loading.value = value;
|
||||
};
|
||||
const toggle = () => {
|
||||
loading.value = !loading.value;
|
||||
};
|
||||
return {
|
||||
loading,
|
||||
setLoading,
|
||||
toggle,
|
||||
};
|
||||
}
|
||||
@@ -21,12 +21,12 @@ export const features = [
|
||||
path: 'search'
|
||||
},
|
||||
{
|
||||
id: 'statistics',
|
||||
id: 'userPersona',
|
||||
title: '阅读画像',
|
||||
desc: '可视化你的知识边界与偏好',
|
||||
icon: TrendTwo,
|
||||
color: '#00b42a',
|
||||
path: 'statistics'
|
||||
path: 'userPersona'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -101,10 +101,6 @@ const { title } = toRefs(props)
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
.animate-float {
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes shine {
|
||||
from {
|
||||
transform: translateX(-100%);
|
||||
@@ -130,4 +126,15 @@ const { title } = toRefs(props)
|
||||
.animate-in {
|
||||
animation: fadeIn 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { getHighlightedSegments } from '../utils/highlighter'
|
||||
|
||||
const props = defineProps<{
|
||||
text: string
|
||||
highlights?: { start: number; length: number }[]
|
||||
}>()
|
||||
|
||||
const segments = computed(() => getHighlightedSegments(props.text, props.highlights || []))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span>
|
||||
<template v-for="(seg, index) in segments" :key="index">
|
||||
<mark v-if="seg.isHighlight" class="highlight-item">{{ seg.text }}</mark>
|
||||
<span v-else>{{ seg.text }}</span>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.highlight-item {
|
||||
background-color: #f5f3ff; /* 浅紫色背景 */
|
||||
color: #7816ff; /* 你的品牌紫色 */
|
||||
font-weight: 700;
|
||||
border-radius: 2px;
|
||||
padding: 0 1px;
|
||||
}
|
||||
</style>
|
||||
191
src/renderer/src/pages/menus/views/search/index.vue
Normal file
191
src/renderer/src/pages/menus/views/search/index.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { Left, Search, Close, DocumentFolder, BookmarkOne } from '@icon-park/vue-next'
|
||||
import useRouterHook from '@renderer/hooks/useRouterHook'
|
||||
import { trpc } from '@renderer/lib/trpc'
|
||||
import { ISearch } from '@shared/types/ISearch' // 假设你已定义了搜索结果接口
|
||||
import HighlightText from './components/HighlightText.vue'
|
||||
import BackPage from '@renderer/components/BackPage.vue'
|
||||
|
||||
const { goBack, go } = useRouterHook()
|
||||
|
||||
const searchQuery = ref('')
|
||||
const searchResults = ref<ISearch[]>([])
|
||||
const isLoading = ref(false)
|
||||
|
||||
// 实时搜索逻辑
|
||||
let searchTimeout: NodeJS.Timeout | null = null
|
||||
watch(searchQuery, (newVal) => {
|
||||
if (searchTimeout) clearTimeout(searchTimeout)
|
||||
if (newVal.trim() === '') {
|
||||
searchResults.value = []
|
||||
return
|
||||
}
|
||||
searchTimeout = setTimeout(() => {
|
||||
performSearch(newVal)
|
||||
}, 300) // 300ms 防抖
|
||||
})
|
||||
|
||||
const performSearch = async (query: string) => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
// 假设 tRPC 提供了 search 接口
|
||||
searchResults.value = await trpc.search.search.query(query)
|
||||
} catch (error) {
|
||||
console.error('搜索失败:', error)
|
||||
searchResults.value = []
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const clearSearch = () => {
|
||||
searchQuery.value = ''
|
||||
searchResults.value = []
|
||||
}
|
||||
|
||||
const goToDetail = (result: ISearch) => {
|
||||
// 假设心得详情页路由是 /reflection/:id
|
||||
go('/reflection', {
|
||||
id: result.id
|
||||
})
|
||||
}
|
||||
|
||||
// 模拟快捷键监听(实际需要 Electron 主进程处理)
|
||||
onMounted(() => {})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full flex flex-col bg-[#F8F9FB] overflow-hidden animate-in">
|
||||
<header
|
||||
class="h-16 px-8 flex items-center justify-between relative z-20 backdrop-blur-md bg-white/70 border-b border-slate-100"
|
||||
>
|
||||
<BackPage title="全局检索" />
|
||||
</header>
|
||||
|
||||
<main class="flex-1 flex flex-col items-center p-8 pt-12 relative overflow-hidden">
|
||||
<div
|
||||
class="w-full max-w-2xl relative flex items-center bg-white rounded-2xl shadow-lg border border-slate-100 focus-within:border-[#7816ff] transition-all duration-200"
|
||||
>
|
||||
<search size="20" class="absolute left-5 text-slate-400" />
|
||||
<input
|
||||
id="search-input"
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="搜索书名、心得、关键词..."
|
||||
class="flex-1 px-14 py-4 text-sm font-medium text-slate-700 bg-transparent outline-none placeholder:text-slate-400"
|
||||
@keydown.esc="clearSearch"
|
||||
/>
|
||||
<transition name="fade">
|
||||
<button
|
||||
v-if="searchQuery"
|
||||
@click="clearSearch"
|
||||
class="absolute right-5 w-6 h-6 rounded-full bg-slate-100 text-slate-500 flex items-center justify-center hover:bg-slate-200 transition-colors"
|
||||
>
|
||||
<close size="12" />
|
||||
</button>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="w-full max-w-2xl mt-8 flex flex-col gap-4 overflow-y-auto custom-scroll transition-all duration-300"
|
||||
:class="{ 'opacity-50': isLoading }"
|
||||
>
|
||||
<div v-if="isLoading" class="flex justify-center items-center py-8 text-slate-400 text-sm">
|
||||
<search size="20" class="animate-pulse mr-2" />
|
||||
正在搜索...
|
||||
</div>
|
||||
<div
|
||||
v-else-if="searchQuery && searchResults.length === 0"
|
||||
class="flex flex-col items-center py-8 text-slate-400 text-sm gap-2"
|
||||
>
|
||||
<document-folder size="32" />
|
||||
<p>没有找到相关的心得</p>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="!searchQuery"
|
||||
class="flex flex-col items-center py-8 text-slate-400 text-sm gap-2"
|
||||
>
|
||||
<bookmark-one size="32" />
|
||||
<p>请输入关键词开始搜索</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="result in searchResults"
|
||||
:key="result.id"
|
||||
@click="goToDetail(result)"
|
||||
class="search-result-card group cursor-pointer"
|
||||
>
|
||||
<div class="flex items-start gap-4">
|
||||
<div
|
||||
class="w-10 h-10 rounded-lg flex-shrink-0 bg-purple-50 flex items-center justify-center"
|
||||
>
|
||||
<bookmark-one size="20" fill="#7816ff" />
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<h3 class="text-sm font-bold text-slate-800 group-hover:text-[#7816ff]">
|
||||
<HighlightText :text="result.title" :highlights="result.titleHighlights" />
|
||||
</h3>
|
||||
<p class="text-[11px] text-slate-500 mt-1 leading-normal line-clamp-2">
|
||||
<HighlightText
|
||||
:text="result.contentSnippet || result.content"
|
||||
:highlights="result.contentHighlights"
|
||||
/>
|
||||
</p>
|
||||
<p class="text-[10px] text-slate-400 mt-2">
|
||||
<span class="font-semibold">{{ result.bookName }}</span> ·
|
||||
<span class="ml-1">{{ new Date(result.createdAt).toLocaleDateString() }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-result-card {
|
||||
@apply bg-white p-4 rounded-xl shadow-sm border border-slate-100 cursor-pointer
|
||||
transition-all duration-200 hover:shadow-md hover:border-[#7816ff]/30 active:scale-[0.99];
|
||||
}
|
||||
|
||||
.custom-scroll::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.custom-scroll::-webkit-scrollbar-thumb {
|
||||
background: #e2e8f0;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.animate-in {
|
||||
animation: fadeIn 0.4s ease-out forwards;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.highlight-item {
|
||||
background-color: #f5f3ff; /* 浅紫色背景 */
|
||||
color: #7816ff; /* 你的品牌紫色 */
|
||||
font-weight: 700;
|
||||
border-radius: 2px;
|
||||
padding: 0 1px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,51 @@
|
||||
// 定义高亮片段的结构
|
||||
export interface HighlightSegment {
|
||||
text: string
|
||||
isHighlight: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 将原始文本根据高亮索引转化为分段数组
|
||||
*/
|
||||
export function getHighlightedSegments(
|
||||
text: string,
|
||||
highlights: { start: number; length: number }[]
|
||||
): HighlightSegment[] {
|
||||
if (!highlights || highlights.length === 0) {
|
||||
return [{ text, isHighlight: false }]
|
||||
}
|
||||
|
||||
// 1. 按照起始位置排序,防止索引乱序导致切片失败
|
||||
const sortedHighlights = [...highlights].sort((a, b) => a.start - b.start)
|
||||
|
||||
const segments: HighlightSegment[] = []
|
||||
let lastIndex = 0
|
||||
|
||||
for (const { start, length } of sortedHighlights) {
|
||||
// 处理匹配项之前的普通文本
|
||||
if (start > lastIndex) {
|
||||
segments.push({
|
||||
text: text.substring(lastIndex, start),
|
||||
isHighlight: false
|
||||
})
|
||||
}
|
||||
|
||||
// 处理匹配到的高亮文本
|
||||
segments.push({
|
||||
text: text.substring(start, start + length),
|
||||
isHighlight: true
|
||||
})
|
||||
|
||||
lastIndex = start + length
|
||||
}
|
||||
|
||||
// 处理剩余的尾部文本
|
||||
if (lastIndex < text.length) {
|
||||
segments.push({
|
||||
text: text.substring(lastIndex),
|
||||
isHighlight: false
|
||||
})
|
||||
}
|
||||
|
||||
return segments
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { ChartGraph, TrendTwo, Local } from '@icon-park/vue-next'
|
||||
import BackPage from '@renderer/components/BackPage.vue'
|
||||
|
||||
const chartRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return
|
||||
const myChart = echarts.init(chartRef.value)
|
||||
|
||||
const option = {
|
||||
radar: {
|
||||
// 这里的指示器可以根据用户本地数据的标签动态生成
|
||||
indicator: [
|
||||
{ name: '认知深度', max: 100 },
|
||||
{ name: '职场技能', max: 100 },
|
||||
{ name: '人文素养', max: 100 },
|
||||
{ name: '技术探索', max: 100 },
|
||||
{ name: '生活哲学', max: 100 }
|
||||
],
|
||||
splitArea: {
|
||||
areaStyle: {
|
||||
color: ['#F8F9FB', '#fff'],
|
||||
shadowColor: 'rgba(0, 0, 0, 0.05)',
|
||||
shadowBlur: 10
|
||||
}
|
||||
},
|
||||
axisLine: { lineStyle: { color: '#E5E7EB' } },
|
||||
splitLine: { lineStyle: { color: '#E5E7EB' } }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'radar',
|
||||
data: [
|
||||
{
|
||||
value: [85, 72, 90, 65, 80], // 这里接入本地统计的真实数据
|
||||
name: '阅读画像',
|
||||
itemStyle: { color: '#7816ff' },
|
||||
areaStyle: { color: 'rgba(120, 22, 255, 0.1)' },
|
||||
lineStyle: { width: 3 }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
myChart.setOption(option)
|
||||
}
|
||||
|
||||
onMounted(() => initChart())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full flex flex-col gap-xl relative overflow-hidden">
|
||||
<header
|
||||
class="h-16 border-b border-slate-100/50 px-6 flex items-center justify-between relative z-20 backdrop-blur-md bg-white/70"
|
||||
>
|
||||
<BackPage title="返回应用菜单" />
|
||||
</header>
|
||||
|
||||
<main class="flex-1 bg-white overflow-y-auto relative z-10 p-4 bg-white rounded-xl">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-purple-50 flex items-center justify-center">
|
||||
<chart-graph theme="outline" size="22" fill="#7816ff" />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm font-bold text-slate-800">个人阅读画像</span>
|
||||
<p class="text-[10px] text-slate-400 uppercase tracking-widest">你的阅读指导专家</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref="chartRef" class="w-full h-[320px]"></div>
|
||||
|
||||
<div class="p-4 bg-[#7816ff]/5 rounded-2xl border border-[#7816ff]/10">
|
||||
<div class="flex items-start gap-3 flex-col">
|
||||
<div class="flex flex-row gap-sm">
|
||||
<trend-two theme="outline" size="16" fill="#7816ff" />
|
||||
<span class="text-sm font-bold text-[#7816ff]">你的阅读分享报告</span>
|
||||
</div>
|
||||
<span class="text-[11px] text-[#7816ff] leading-relaxed font-medium">
|
||||
基于你最近生成的 12
|
||||
篇心得分析:你在<strong>人文素养</strong>领域积累深厚,近期对<strong>认知科学</strong>的关注度上升了
|
||||
24%。
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
192
src/renderer/src/pages/menus/views/userPersona/index.vue
Normal file
192
src/renderer/src/pages/menus/views/userPersona/index.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { ChartGraph, TrendTwo, Refresh } from '@icon-park/vue-next'
|
||||
import BackPage from '@renderer/components/BackPage.vue'
|
||||
import { trpc } from '@renderer/lib/trpc'
|
||||
import { IUserReadingPersona } from '@shared/types/IUserReadingPersona'
|
||||
import useLoading from '@renderer/hooks/useLoading'
|
||||
|
||||
const chartRef = ref<HTMLElement | null>(null)
|
||||
const personaData = ref<IUserReadingPersona | null>(null)
|
||||
const { loading, setLoading } = useLoading()
|
||||
let myChart: echarts.ECharts | null = null
|
||||
|
||||
// 核心方法:初始化或更新图表
|
||||
const renderChart = (data: IUserReadingPersona) => {
|
||||
if (!chartRef.value) return
|
||||
if (!myChart) myChart = echarts.init(chartRef.value)
|
||||
|
||||
const option = {
|
||||
radar: {
|
||||
indicator: [
|
||||
{ name: '认知深度', max: 100 },
|
||||
{ name: '产出效率', max: 100 }, // 对应 efficiencyScore
|
||||
{ name: '成熟度', max: 100 }, // 对应 maturityScore
|
||||
{ name: '知识广度', max: 100 }, // 对应 breadthScore
|
||||
{ name: '语言能力', max: 100 } // 对应 languageScore
|
||||
],
|
||||
shape: 'circle',
|
||||
splitArea: {
|
||||
areaStyle: {
|
||||
color: ['#F8F9FB', '#fff'],
|
||||
shadowColor: 'rgba(0, 0, 0, 0.05)',
|
||||
shadowBlur: 10
|
||||
}
|
||||
},
|
||||
axisLine: { lineStyle: { color: '#E5E7EB' } },
|
||||
splitLine: { lineStyle: { color: '#E5E7EB' } }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'radar',
|
||||
data: [
|
||||
{
|
||||
value: [
|
||||
// 按照 indicator 的顺序填入分值
|
||||
data.domainDepth[0]?.score || 0,
|
||||
data.efficiencyScore,
|
||||
data.maturityScore,
|
||||
data.breadthScore,
|
||||
data.languageScore
|
||||
],
|
||||
name: '阅读画像',
|
||||
itemStyle: { color: '#7816ff' },
|
||||
areaStyle: { color: 'rgba(120, 22, 255, 0.15)' },
|
||||
lineStyle: { width: 3 }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
myChart.setOption(option)
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async (force = false) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
if (force) {
|
||||
await trpc.persona.forceRefreshUserPersona.mutate()
|
||||
}
|
||||
const res = await trpc.persona.getUserPersona.query()
|
||||
if (res) {
|
||||
personaData.value = res
|
||||
await nextTick()
|
||||
renderChart(res)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取画像失败:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
// 窗口缩放自适应
|
||||
window.addEventListener('resize', () => myChart?.resize())
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full flex flex-col gap-xl relative overflow-hidden bg-[#F8F9FB]">
|
||||
<header
|
||||
class="h-16 border-b border-slate-100/50 px-6 flex items-center justify-between relative z-20 backdrop-blur-md bg-white/70"
|
||||
>
|
||||
<BackPage title="返回应用菜单" />
|
||||
|
||||
<a-button
|
||||
@click="fetchData(true)"
|
||||
:disabled="loading"
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-bold transition-all active:scale-95"
|
||||
:class="loading ? 'text-slate-300' : 'text-[#7816ff] hover:bg-purple-50'"
|
||||
>
|
||||
<refresh :class="{ 'animate-spin': loading }" />
|
||||
{{ loading ? '同步中...' : '同步最新画像' }}
|
||||
</a-button>
|
||||
</header>
|
||||
|
||||
<main class="flex-1 overflow-y-auto relative z-10">
|
||||
<div class="max-w-4xl mx-auto space-y-6">
|
||||
<div class="bg-white p-8 rounded-xl shadow-sm border border-slate-100">
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-2xl bg-purple-50 flex items-center justify-center">
|
||||
<chart-graph theme="outline" size="26" fill="#7816ff" />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-lg font-black text-slate-800 tracking-tight">阅读画像</span>
|
||||
<p class="text-[10px] text-slate-400 uppercase tracking-widest font-bold">
|
||||
让每一篇阅读者享受阅读与思考的乐趣
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--用户画像展示-->
|
||||
<div v-show="personaData" ref="chartRef" class="w-full h-[300px]"></div>
|
||||
|
||||
<!--暂无数据提示-->
|
||||
<div
|
||||
v-if="!personaData && !isLoading"
|
||||
class="h-[300px] flex flex-col items-center justify-center text-slate-400 gap-4"
|
||||
>
|
||||
<div class="w-20 h-20 bg-slate-50 rounded-full flex items-center justify-center">
|
||||
<chart-graph size="32" />
|
||||
</div>
|
||||
<p class="text-sm font-medium">暂无画像数据,请点击上方同步</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="personaData"
|
||||
class="p-6 bg-[#7816ff] rounded-[32px] text-white shadow-xl shadow-purple-200/50 relative overflow-hidden"
|
||||
>
|
||||
<div class="absolute -right-10 -top-10 w-40 h-40 bg-white/10 rounded-full blur-3xl"></div>
|
||||
|
||||
<div class="relative z-10 flex flex-col">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<trend-two theme="outline" size="20" fill="#fff" />
|
||||
<span class="text-md font-black">智能阅读报告</span>
|
||||
</div>
|
||||
|
||||
<p class="text-[13px] leading-relaxed opacity-90 font-medium">
|
||||
基于你累计生成的
|
||||
<span class="text-yellow-300 font-bold">{{ personaData.stats.totalWords }}</span>
|
||||
字心得分析: 你在
|
||||
<span class="underline underline-offset-4 decoration-yellow-300">{{
|
||||
personaData.domainDepth[0]?.name || '未知领域'
|
||||
}}</span>
|
||||
领域表现出极高的探索欲。
|
||||
</p>
|
||||
|
||||
<div class="flex flex-wrap gap-2 mt-2">
|
||||
<span
|
||||
v-for="kw in personaData.stats.topKeywords"
|
||||
:key="kw"
|
||||
class="text-[10px] bg-white/20 px-3 py-1 rounded-full backdrop-blur-md"
|
||||
>
|
||||
# {{ kw }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
</style>
|
||||
@@ -26,9 +26,14 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import('@renderer/pages/reflection/views/poster/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/menus/statistics',
|
||||
name: 'ReadingStatistics',
|
||||
component: () => import('@renderer/pages/menus/views/statistics/index.vue')
|
||||
path: '/menus/userPersona',
|
||||
name: 'ReadingUserPersona',
|
||||
component: () => import('@renderer/pages/menus/views/userPersona/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/menus/search',
|
||||
name: 'ReadingSearch',
|
||||
component: () => import('@renderer/pages/menus/views/search/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/menus/:pathMatch(.*)*',
|
||||
|
||||
Reference in New Issue
Block a user