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

1
auto-imports.d.ts vendored
View File

@ -70,7 +70,6 @@ declare global {
const useCssVars: typeof import('vue')['useCssVars'] const useCssVars: typeof import('vue')['useCssVars']
const useDialog: typeof import('naive-ui')['useDialog'] const useDialog: typeof import('naive-ui')['useDialog']
const useId: typeof import('vue')['useId'] const useId: typeof import('vue')['useId']
const useLink: typeof import('vue-router')['useLink']
const useLoadingBar: typeof import('naive-ui')['useLoadingBar'] const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
const useMessage: typeof import('naive-ui')['useMessage'] const useMessage: typeof import('naive-ui')['useMessage']
const useModel: typeof import('vue')['useModel'] const useModel: typeof import('vue')['useModel']

4
components.d.ts vendored
View File

@ -17,8 +17,12 @@ declare module 'vue' {
Mask: typeof import('./src/components/mask.vue')['default'] Mask: typeof import('./src/components/mask.vue')['default']
MenuH: typeof import('./src/components/menuH.vue')['default'] MenuH: typeof import('./src/components/menuH.vue')['default']
NAvatar: typeof import('naive-ui')['NAvatar'] NAvatar: typeof import('naive-ui')['NAvatar']
NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
NButton: typeof import('naive-ui')['NButton'] NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard'] NCard: typeof import('naive-ui')['NCard']
NCollapse: typeof import('naive-ui')['NCollapse']
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
NConfigProvider: typeof import('naive-ui')['NConfigProvider'] NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDialogProvider: typeof import('naive-ui')['NDialogProvider'] NDialogProvider: typeof import('naive-ui')['NDialogProvider']
NDropdown: typeof import('naive-ui')['NDropdown'] NDropdown: typeof import('naive-ui')['NDropdown']

View File

@ -8,7 +8,7 @@
}, },
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "run-p type-check \"build-only {@}\" --", "b": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview", "preview": "vite preview",
"build-only": "vite build", "build-only": "vite build",
"type-check": "vue-tsc --build" "type-check": "vue-tsc --build"
@ -41,7 +41,7 @@
"naive-ui": "^2.43.2", "naive-ui": "^2.43.2",
"npm-run-all2": "^8.0.4", "npm-run-all2": "^8.0.4",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"typescript": "~5.8.0", "typescript": "^5.9.3",
"vfonts": "^0.0.3", "vfonts": "^0.0.3",
"vite": "^7.0.6", "vite": "^7.0.6",
"vite-plugin-vue-devtools": "^8.0.0", "vite-plugin-vue-devtools": "^8.0.0",

3
src/icon/star.svg Normal file
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

View File

@ -6,20 +6,56 @@
<MdCatalog :editorId="blogData.aid" class="my-cata" :scrollElement="scrollElement" /> <MdCatalog :editorId="blogData.aid" class="my-cata" :scrollElement="scrollElement" />
</div> </div>
</div> </div>
<div ref="" class="flex-1"> <div class="flex-1">
<div class="w-full px-1/20 rounded-md"> <n-breadcrumb class="m-2" separator="|">
<div class="text-center text-[40px] text-[700] text-black` mt-10">{{ blogData.title }}</div> <n-breadcrumb-item href="/blog/blog">
<div class="flex flex-wrap gap-4 justify-center mt-4"> 文章
</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"> <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> <n-icon><icon-tag /></n-icon>
{{ item }} {{ item }}
</div> </div>
</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" <MdPreview ref="mdp" theme="light" class="relative " :editorId="blogData.aid" previewTheme="my"
:modelValue="markdown" /> :modelValue="markdown" />
</div> </div>
@ -28,17 +64,6 @@
</template> </template>
<script setup lang="ts"> <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({ definePage({
name: 'blog/:bid', name: 'blog/:bid',
path: '/blog/:bid', path: '/blog/:bid',
@ -46,6 +71,20 @@ definePage({
title: '文章详情', 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 markdown = ref('');
const blogData = ref<any>({ const blogData = ref<any>({
aid: 'preview-only', aid: 'preview-only',
@ -62,8 +101,46 @@ async function getBlogDetail() {
tags.value = ts.split(',') tags.value = ts.split(',')
editorRef.value?.toggleCatalog(true) editorRef.value?.toggleCatalog(true)
markdown.value = res.data.cont 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(() => { onMounted(() => {
getBlogDetail() getBlogDetail()
}) })
@ -106,7 +183,7 @@ onMounted(() => {
} }
.md-editor-catalog-link { .md-editor-catalog-link {
padding-block:1px !important; padding-block: 1px !important;
} }
.md-editor-catalog-wrapper span { .md-editor-catalog-wrapper span {

View File

@ -11,6 +11,7 @@
], ],
"exclude": ["src/**/__tests__/*"], "exclude": ["src/**/__tests__/*"],
"compilerOptions": { "compilerOptions": {
"target": "es2021",
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"types":["unplugin-vue-router/client"], "types":["unplugin-vue-router/client"],
"paths": { "paths": {

View File

@ -11,7 +11,8 @@
"compilerOptions": { "compilerOptions": {
"noEmit": true, "noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2021",
"lib": ["es2021.string", "dom"],
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Bundler", "moduleResolution": "Bundler",
"types": ["node"] "types": ["node"]

View File

@ -4020,10 +4020,10 @@ tslib@^2.1.0, tslib@^2.3.0:
resolved "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" resolved "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
typescript@~5.8.0: typescript@^5.9.3:
version "5.8.3" version "5.9.3"
resolved "https://registry.npmmirror.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e" resolved "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
uc.micro@^1.0.1, uc.micro@^1.0.5: uc.micro@^1.0.1, uc.micro@^1.0.5:
version "1.0.6" version "1.0.6"