feat: 添加AI摘要功能并优化博客详情页布局

This commit is contained in:
2026-01-27 14:51:44 +08:00
parent ac4f8dac82
commit 4704fe81e9
8 changed files with 114 additions and 29 deletions
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
<path fill-rule="evenodd" d="M9 4.5a.75.75 0 0 1 .721.544l.813 2.846a3.75 3.75 0 0 0 2.576 2.576l2.846.813a.75.75 0 0 1 0 1.442l-2.846.813a3.75 3.75 0 0 0-2.576 2.576l-.813 2.846a.75.75 0 0 1-1.442 0l-.813-2.846a3.75 3.75 0 0 0-2.576-2.576l-2.846-.813a.75.75 0 0 1 0-1.442l2.846-.813A3.75 3.75 0 0 0 7.466 7.89l.813-2.846A.75.75 0 0 1 9 4.5ZM18 1.5a.75.75 0 0 1 .728.568l.258 1.036c.236.94.97 1.674 1.91 1.91l1.036.258a.75.75 0 0 1 0 1.456l-1.036.258c-.94.236-1.674.97-1.91 1.91l-.258 1.036a.75.75 0 0 1-1.456 0l-.258-1.036a2.625 2.625 0 0 0-1.91-1.91l-1.036-.258a.75.75 0 0 1 0-1.456l1.036-.258a2.625 2.625 0 0 0 1.91-1.91l.258-1.036A.75.75 0 0 1 18 1.5ZM16.5 15a.75.75 0 0 1 .712.513l.394 1.183c.15.447.5.799.948.948l1.183.395a.75.75 0 0 1 0 1.422l-1.183.395c-.447.15-.799.5-.948.948l-.395 1.183a.75.75 0 0 1-1.422 0l-.395-1.183a1.5 1.5 0 0 0-.948-.948l-1.183-.395a.75.75 0 0 1 0-1.422l1.183-.395c.447-.15.799-.5.948-.948l.395-1.183A.75.75 0 0 1 16.5 15Z" clip-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+98 -21
View File
@@ -6,20 +6,56 @@
<MdCatalog :editorId="blogData.aid" class="my-cata" :scrollElement="scrollElement" />
</div>
</div>
<div ref="" class="flex-1">
<div class="w-full px-1/20 rounded-md">
<div class="text-center text-[40px] text-[700] text-black` mt-10">{{ blogData.title }}</div>
<div class="flex flex-wrap gap-4 justify-center mt-4">
<div class="flex-1">
<n-breadcrumb class="m-2" separator="|">
<n-breadcrumb-item href="/blog/blog">
文章
</n-breadcrumb-item>
<n-breadcrumb-item>
{{ blogData.title }}
</n-breadcrumb-item>
</n-breadcrumb>
<div class="w-full px-1/20">
<!-- <div class="text-center text-[40px] font-bold text-black` mt-10">{{ blogData.title }}</div> -->
<div class="my-4 bg-white shadow-lg p-4 rounded-md">
<n-collapse @item-header-click="AISum" arrow-placement="right">
<n-collapse-item>
<template #arrow>
<div class=" text-primary ml-2">
<!-- <icon-star class="w-6" /> -->
</div>
</template>
<template #header>
<div class="text-primary flex">
<icon-star class="w-6 mx-2" />
<div class="text-center text-[20px] font-bold">AI 摘要</div>
</div>
</template>
<template #header-extra>
<div class="text-primary flex">
<icon-star class="w-6 mx-2" />
</div>
</template>
<div class="indent-lg text-gray-500">
{{ msg.replaceAll('\\n', '<br>') }}
</div>
</n-collapse-item>
</n-collapse>
</div>
<div class="flex gap-2 justify-center my-4">
<em class="text-primary">
{{ blogData.nickname }}</em>
<em class="time">{{ formatTime(blogData.updated_at, "YYYY年MM月DD日hh时") }}</em>
</div>
<div class="flex flex-wrap gap-4 justify-center my-4">
<div v-for="item in tags" :key="item.tid" class="flex items-center gap-1 text-primary cursor-pointer">
<n-icon><icon-tag /></n-icon>
{{ item }}
</div>
</div>
<div class="flex gap-2 justify-end my-4">
<em class="text-primary">
{{ blogData.nickname }}</em>
<em class="time">{{ formatTime(blogData.updated_at, "YYYY年MM月DD日hh时") }}</em>
</div>
<MdPreview ref="mdp" theme="light" class="relative " :editorId="blogData.aid" previewTheme="my"
:modelValue="markdown" />
</div>
@@ -28,17 +64,6 @@
</template>
<script setup lang="ts">
import { formatTime } from '@/util';
import type { ExposeParam } from 'md-editor-v3';
import { MdCatalog, MdPreview } from 'md-editor-v3';
const editorRef = ref<ExposeParam>();
const scrollElement: any = document.querySelector('.n-scrollbar .n-scrollbar-container');
const tags = ref<any[]>([])
const route: any = useRoute()
definePage({
name: 'blog/:bid',
path: '/blog/:bid',
@@ -46,6 +71,20 @@ definePage({
title: '文章详情',
}
})
import { formatTime } from '@/util';
import type { ExposeParam } from 'md-editor-v3';
import { MdCatalog, MdPreview } from 'md-editor-v3';
const editorRef = ref<ExposeParam>();
const scrollElement: HTMLElement = document.querySelector('.n-scrollbar .n-scrollbar-container') as HTMLElement;
const tags = ref<any[]>([])
const route: any = useRoute()
const msg = ref<any>('...')
const aimask = ref(false)
const markdown = ref('');
const blogData = ref<any>({
aid: 'preview-only',
@@ -62,8 +101,46 @@ async function getBlogDetail() {
tags.value = ts.split(',')
editorRef.value?.toggleCatalog(true)
markdown.value = res.data.cont
// AISum()
}
// ai总结
async function AISum() {
console.log('>>> --> AISum --> AISum:', AISum)
if (aimask.value) return
const response = await fetch('https://api.siliconflow.cn/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer sk-jwwmhmxsjtseyekknqmamlvzmrkmwfvuacnssbwfufogrkdg'
},
body: JSON.stringify({
model: "Qwen/Qwen3-8B",
messages: [{ role: 'user', content: "用一段话对以下内容做一个摘要(语气可爱带表情):" + blogData.value.cont }],
stream: true
})
})
console.log('>>> --> AISum --> response:', response)
const reader = response.body?.getReader()
const decoder = new TextDecoder('utf-8')
if (!reader) return
msg.value = ''
let done = false
while (!done) {
const { value, done: doneReading } = await reader.read()
done = doneReading
const chunk = decoder.decode(value)
const match = chunk.match(/"content":"(.*?)"/)
if (match && match[1]) {
msg.value += match[1]
aimask.value = true
}
}
// const data = await response.json()
// console.log('>>> --> AISum --> data:', data)
}
onMounted(() => {
getBlogDetail()
})
@@ -106,7 +183,7 @@ onMounted(() => {
}
.md-editor-catalog-link {
padding-block:1px !important;
padding-block: 1px !important;
}
.md-editor-catalog-wrapper span {