集成AI摘要

This commit is contained in:
nojava
2026-05-21 16:01:34 +08:00
parent 1bce12927e
commit 14c3f98449
14 changed files with 1035 additions and 6 deletions
+4
View File
@@ -36,6 +36,10 @@ export function toggleArticleLike(id) {
return request.post(`/api/app/article/${id}/like`)
}
export function generateArticleSummary(data) {
return request.post('/api/app/article/summary', data, { timeout: 35000 })
}
export function getCategories() {
return request.get('/api/public/category/list')
}
+80 -3
View File
@@ -32,7 +32,22 @@
</div>
</el-form-item>
<el-form-item label="摘要">
<el-input v-model="form.summary" type="textarea" :rows="2" placeholder="文章摘要" />
<div class="summary-wrapper">
<el-input v-model="form.summary" type="textarea" :rows="3" placeholder="文章摘要(可手动撰写,也可点击下方按钮由 AI 生成)" />
<div class="summary-actions">
<el-button
type="primary"
size="small"
:loading="summarizing"
@click="handleGenerateSummary"
>
AI 生成摘要
</el-button>
<span v-if="lastSummaryMeta" class="summary-meta">
{{ lastSummaryMeta }}
</span>
</div>
</div>
</el-form-item>
<el-form-item label="可见性">
<el-radio-group v-model="form.visibility">
@@ -55,13 +70,15 @@ import { useRoute, useRouter } from 'vue-router'
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import Placeholder from '@tiptap/extension-placeholder'
import { createArticle, updateArticle, getArticleDetail, getCategories, getTags } from '@/api/article'
import { ElMessage } from 'element-plus'
import { createArticle, updateArticle, getArticleDetail, getCategories, getTags, generateArticleSummary } from '@/api/article'
import { ElMessage, ElMessageBox } from 'element-plus'
const route = useRoute()
const router = useRouter()
const isEdit = computed(() => !!route.params.id)
const saving = ref(false)
const summarizing = ref(false)
const lastSummaryMeta = ref('')
const categories = ref([])
const tags = ref([])
const form = ref({
@@ -73,6 +90,53 @@ const form = ref({
allowComment: true,
})
const MIN_CONTENT_LENGTH = 20
function getPlainTextContent() {
const html = editor.value?.getHTML() || ''
const tmp = document.createElement('div')
tmp.innerHTML = html
return (tmp.textContent || tmp.innerText || '').trim()
}
async function handleGenerateSummary() {
const plain = getPlainTextContent()
if (plain.length < MIN_CONTENT_LENGTH) {
ElMessage.warning('正文过短,无法生成摘要')
return
}
if (form.value.summary && form.value.summary.trim()) {
try {
await ElMessageBox.confirm('已有摘要,确认覆盖?', '提示', {
confirmButtonText: '覆盖',
cancelButtonText: '取消',
type: 'warning',
})
} catch {
return
}
}
summarizing.value = true
lastSummaryMeta.value = ''
try {
const res = await generateArticleSummary({
title: form.value.title || '',
content: plain,
})
form.value.summary = res.data.summary
const meta = res.data
lastSummaryMeta.value = `模型: ${meta.model || '-'} 耗时: ${meta.costMillis ?? '-'}ms` +
(meta.promptTokens != null ? ` tokens: ${meta.promptTokens}/${meta.completionTokens}` : '')
ElMessage.success('摘要生成成功')
} catch (err) {
if (err?.code === 'ECONNABORTED') {
ElMessage.error('AI 服务响应超时,请稍后再试')
}
} finally {
summarizing.value = false
}
}
const editor = useEditor({
extensions: [
StarterKit,
@@ -193,4 +257,17 @@ onBeforeUnmount(() => {
color: var(--text-secondary);
margin: var(--spacing-md) 0;
}
.summary-wrapper {
width: 100%;
}
.summary-actions {
margin-top: var(--spacing-sm);
display: flex;
align-items: center;
gap: var(--spacing-md);
}
.summary-meta {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
</style>