@@ -60,14 +60,37 @@
< / button >
< / view >
< view v-else class = "empty" > 本次回答没有可展示的引用 , 不补造来源 。 < / view >
< button v-if = "answer.citations?.length" class="secondary-button outline-create" :disabled="outlineBusy" @click="generateOutline" >
{{ outlineBusy ? ' 正在生成提纲 ' : ' 生成汇报提纲 ' }}
< / button >
< / view >
< view v-if = "outline" class="panel employee-card outline-panel" >
< view class = "outline-head" > < text class = "panel-title" > 汇报提纲 < / text > < text class = "outline-state" > { { outline . confirmed ? '已确认' : '可编辑' } } < / text > < / view >
< input v-model = "outline.title" class="outline-input outline-title" :disabled="outline.confirmed" maxlength="120" placeholder="汇报标题" / >
< view v-for = "(section, sectionIndex) in outline.sections" :key="sectionIndex" class="outline-section" >
< input v-model = "section.title" class="outline-input section-title" :disabled="outline.confirmed" maxlength="80" placeholder="章节标题" / >
< input v-for = "(_bullet, bulletIndex) in section.bullets" :key="bulletIndex" v-model="section.bullets[bulletIndex]" class="outline-input bullet-input" :disabled="outline.confirmed" maxlength="240" placeholder="要点" / >
< button v-if = "!outline.confirmed" class="text-button" @click="addOutlineBullet(sectionIndex)" > + 添加要点 < / button >
< / view >
< button v-if = "!outline.confirmed" class="text-button" @click="addOutlineSection" > + 添加章节 < / button >
< view class = "outline-sources" >
< text class = "citation-heading" > 资料来源不可编辑 < / text >
< text v-for = "source in outline.sources" :key="`${source.domain}-${source.sourceId}`" class="outline-source" > {{ source.domain = = = ' ENTERPRISE ' ? ' 企业知识 ' : ' 我的资料 ' }} · {{ source.title }} < / text >
< / view >
< view v-if = "!outline.confirmed" class="outline-actions" >
< button class = "secondary-button" :disabled = "outlineBusy" @click ="saveOutline" > 保存修改 < / button >
< button class = "primary-button" :disabled = "outlineBusy" @click ="confirmOutline" > 确认大纲 < / button >
< / view >
< text v-if = "outlineMessage" class="status-message" > {{ outlineMessage }} < / text >
< / view >
< / view >
< / template >
< script setup lang = "ts" >
import { computed , ref } from 'vue' ;
import type { PersonalAskResponse , PersonalCitation } from '@/types/api' ;
import { askPersonalAssistant , type KnowledgeScope } from '@/services/personal-assistant' ;
import type { PersonalAskResponse , PersonalCitation , PersonalExportOutline } from '@/types/api' ;
import { askPersonalAssistant , confirmPersonalOutline , createPersonalOutline , updatePersonalOutline , type KnowledgeScope } from '@/services/personal-assistant' ;
import { personalItemRoute } from '@/services/personal-assistant-id' ;
const scopes = ref < KnowledgeScope [ ] > ( [ 'PERSONAL' ] ) ;
@@ -75,6 +98,9 @@ const queryText = ref('');
const asking = ref ( false ) ;
const answer = ref < PersonalAskResponse > ( ) ;
const message = ref ( '' ) ;
const outline = ref < PersonalExportOutline > ( ) ;
const outlineBusy = ref ( false ) ;
const outlineMessage = ref ( '' ) ;
let requestGeneration = 0 ;
const enterpriseEnabled = computed ( ( ) => scopes . value . includes ( 'ENTERPRISE' ) ) ;
@@ -83,6 +109,7 @@ const changeScope = () => {
if ( asking . value ) return ;
requestGeneration += 1 ;
answer . value = undefined ;
outline . value = undefined ;
message . value = '' ;
scopes . value = enterpriseEnabled . value ? [ 'PERSONAL' ] : [ 'PERSONAL' , 'ENTERPRISE' ] ;
} ;
@@ -94,6 +121,7 @@ const submitQuestion = async () => {
const scopeSnapshot = [ ... scopes . value ] ;
asking . value = true ;
answer . value = undefined ;
outline . value = undefined ;
message . value = '' ;
try {
const response = await askPersonalAssistant ( { queryText : question , scope : scopeSnapshot , outputFormat : 'ACTION_PLAN' } ) ;
@@ -123,6 +151,64 @@ const submitQuestion = async () => {
}
} ;
const generateOutline = async ( ) => {
if ( ! answer . value ? . sessionId || ! answer . value . citations ? . length || outlineBusy . value ) return ;
outlineBusy . value = true ;
outlineMessage . value = '' ;
try {
outline . value = await createPersonalOutline ( { sessionId : answer . value . sessionId , title : queryText . value . trim ( ) } ) ;
} catch ( error ) {
outlineMessage . value = error instanceof Error ? error . message : '提纲生成失败' ;
} finally {
outlineBusy . value = false ;
}
} ;
const outlinePayload = ( ) => outline . value ? {
title : outline . value . title . trim ( ) ,
sections : outline . value . sections . map ( ( section ) => ( {
title : section . title . trim ( ) ,
bullets : section . bullets . map ( ( bullet ) => bullet . trim ( ) ) . filter ( Boolean )
} ) ) . filter ( ( section ) => section . title && section . bullets . length )
} : undefined ;
const saveOutline = async ( ) => {
const payload = outlinePayload ( ) ;
if ( ! outline . value || ! payload || ! payload . title || ! payload . sections . length || outlineBusy . value ) {
outlineMessage . value = '请保留标题和至少一个有效章节' ;
return ;
}
outlineBusy . value = true ;
outlineMessage . value = '' ;
try {
outline . value = await updatePersonalOutline ( outline . value . id , payload ) ;
outlineMessage . value = '提纲已保存' ;
} catch ( error ) {
outlineMessage . value = error instanceof Error ? error . message : '提纲保存失败' ;
} finally {
outlineBusy . value = false ;
}
} ;
const confirmOutline = async ( ) => {
await saveOutline ( ) ;
if ( ! outline . value || outlineMessage . value !== '提纲已保存' ) return ;
outlineBusy . value = true ;
try {
await confirmPersonalOutline ( outline . value . id ) ;
outline . value . confirmed = true ;
outline . value . status = 'CONFIRMED' ;
outlineMessage . value = '大纲已确认,内容已冻结' ;
} catch ( error ) {
outlineMessage . value = error instanceof Error ? error . message : '大纲确认失败' ;
} finally {
outlineBusy . value = false ;
}
} ;
const addOutlineSection = ( ) => outline . value ? . sections . push ( { title : '' , bullets : [ '' ] } ) ;
const addOutlineBullet = ( sectionIndex : number ) => outline . value ? . sections [ sectionIndex ] ? . bullets . push ( '' ) ;
const citationLabel = ( citation : PersonalCitation ) => {
if ( citation . domain === 'ENTERPRISE' ) return '企业 SOP' ;
return citation . sourceType === 'URL' ? '外部网页' : '我的资料' ;
@@ -160,4 +246,5 @@ const openSessions = () => uni.navigateTo({ url: '/pages/user/assistant/sessions
. domain - badge . enterprise { background : # fff2e6 ; color : var ( -- warning ) ; }
. citation - title { min - width : 0 ; overflow : hidden ; font - weight : 800 ; text - overflow : ellipsis ; white - space : nowrap ; }
. citation - excerpt { margin - top : 7 px ; color : var ( -- muted ) ; font - size : 13 px ; line - height : 1.55 ; }
. outline - create { width : 100 % ; margin - top : 14 px ; } . outline - head { display : flex ; align - items : center ; justify - content : space - between ; gap : 12 px ; } . outline - state { color : var ( -- brand ) ; font - size : 12 px ; font - weight : 800 ; } . outline - input { width : 100 % ; min - height : 42 px ; margin - top : 10 px ; padding : 0 12 px ; border : 1 px solid var ( -- line ) ; background : # fff ; color : var ( -- ink ) ; } . outline - title { font - size : 17 px ; font - weight : 900 ; } . outline - section { margin - top : 14 px ; padding : 12 px ; border : 1 px solid var ( -- line ) ; background : # f8fafc ; } . section - title { margin - top : 0 ; font - weight : 800 ; } . bullet - input { font - size : 13 px ; } . text - button { margin - top : 8 px ; padding : 4 px 0 ; border : 0 ; background : transparent ; color : var ( -- brand ) ; font - size : 13 px ; } . outline - sources { margin - top : 14 px ; } . outline - source { display : block ; margin - top : 6 px ; color : var ( -- muted ) ; font - size : 12 px ; } . outline - actions { display : grid ; grid - template - columns : 1 fr 1 fr ; gap : 10 px ; margin - top : 16 px ; } . outline - actions button { width : 100 % ; }
< / style >