@@ -25,6 +25,18 @@
< button v-if = "item.sourceType === 'FILE' && item.status === 'READY'" class="secondary-button full-button" :disabled="busy" @click="download" > 下载原附件 < / button >
< / view >
< view v-if = "item.summary" class="panel employee-card"><text class="panel-title">内容摘要</text><text class="summary" > {{ item.summary }} < / text > < / view >
< view v-if = "item.status === 'READY'" class="panel employee-card publish-card" >
< text class = "panel-title" > 沉淀企业知识 < / text >
< text class = "status-copy" > 提交后由 HR / 运营审核 ; 批准时只生成脱敏企业副本 , 个人资料仍归你所有 。 < / text >
< template v-if = "publishRequest" >
< text class = "publish-status" > { { publishStatusText ( publishRequest . status ) } } < / text >
< text v-if = "publishRequest.reviewComment" class="meta" > 审核意见 : {{ publishRequest.reviewComment }} < / text >
< / template >
< template v-if = "!publishRequest || publishRequest.status === 'REJECTED'" >
< input v-model = "publishReason" class="publish-input" maxlength="500" placeholder="说明为什么值得沉淀为企业知识" / >
< button class = "primary-button full-button" : disabled = "busy || !publishReason.trim()" @click ="submitPublishRequest" > 申请沉淀企业知识 < / button >
< / template >
< / view >
< view class = "panel danger-panel" >
< text class = "panel-title danger-title" > 删除资料 < / text >
< text class = "danger-copy" > 删除后 , 附件 、 解析正文和搜索索引会清除 , 且无法恢复 。 < / text >
@@ -38,22 +50,25 @@
< script setup lang = "ts" >
import { ref } from 'vue' ;
import { onLoad , onUnload } from '@dcloudio/uni-app' ;
import type { PersonalItem , PersonalItemSourceType , PersonalItemStatus } from '@/types/api' ;
import { deletePersonalItem , downloadPersonalItem , loadPersonalItem , retryPersonalItem , retryPersonalOcrPages } from '@/services/personal-assistant' ;
import type { PersonalItem , PersonalItemSourceType , PersonalItemStatus , PersonalPublishRequest } from '@/types/api' ;
import { deletePersonalItem , downloadPersonalItem , loadPersonalItem , loadPersonalPublishRequests , requestPersonalPublish , retryPersonalItem , retryPersonalOcrPages } from '@/services/personal-assistant' ;
import { normalizeBackendId } from '@/services/personal-assistant-id' ;
import { isOcrActive , ocrProgressPercent , ocrProgressText } from '@/services/personal-assistant-ocr' ;
const itemId = ref ( '' ) ; const item = ref < PersonalItem > ( ) ; const loading = ref ( false ) ; const busy = ref ( false ) ; const message = ref ( '' ) ;
const publishRequest = ref < PersonalPublishRequest > ( ) ; const publishReason = ref ( '' ) ;
let pollTimer : ReturnType < typeof setTimeout > | undefined ;
const statusText = ( status : PersonalItemStatus ) => ( { QUEUED : '等待处理' , PARSING : '解析中' , READY : '可检索' , FAILED : '解析失败' , DELETING : '删除中' , DELETED : '已删除' } ) [ status ] ;
const statusDescription = ( status : PersonalItemStatus ) => ( { QUEUED : '已进入解析队列。' , PARSING : '正在提取正文和建立搜索索引。' , READY : '已完成解析,可用于个人助理问答。' , FAILED : '本次解析失败,可查看公开原因后重试。' , DELETING : '正在清理附件、正文和索引。' , DELETED : '资料已删除。' } ) [ status ] ;
const sourceText = ( source : PersonalItemSourceType ) => ( { TEXT : '文字' , FILE : '文件' , URL : '外部网页' } ) [ source ] ;
const stopPolling = ( ) => { if ( pollTimer ) clearTimeout ( pollTimer ) ; pollTimer = undefined ; } ;
const schedulePolling = ( ) => { stopPolling ( ) ; if ( item . value && ( [ 'QUEUED' , 'PARSING' ] . includes ( item . value . status ) || isOcrActive ( item . value . ocr ) ) ) pollTimer = setTimeout ( ( ) => void load ( true ) , 2000 ) ; } ;
const load = async ( silent = false ) => { if ( ! itemId . value ) return ; if ( ! silent ) loading . value = true ; message . value = '' ; try { item . value = await loadPersonalItem ( itemId . value ) ; schedulePolling ( ) ; } catch ( error ) { message . value = error instanceof Error ? error . message : '资料不存在或无权访问' ; stopPolling ( ) ; } finally { if ( ! silent ) loading . value = false ; } } ;
const load = async ( silent = false ) => { if ( ! itemId . value ) return ; if ( ! silent ) loading . value = true ; message . value = '' ; try { item . value = await loadPersonalItem ( itemId . value ) ; const requests = await loadPersonalPublishRequests ( ) ; publishRequest . value = requests . find ( ( request ) => request . itemId === itemId . value ) ; schedulePolling ( ) ; } catch ( error ) { message . value = error instanceof Error ? error . message : '资料不存在或无权访问' ; stopPolling ( ) ; } finally { if ( ! silent ) loading . value = false ; } } ;
const retry = async ( ) => { busy . value = true ; message . value = '' ; try { item . value = await retryPersonalItem ( itemId . value ) ; } catch ( error ) { message . value = error instanceof Error ? error . message : '重试失败' ; } finally { busy . value = false ; } } ;
const retryOcrPages = async ( ) => { busy . value = true ; message . value = '' ; try { item . value = await retryPersonalOcrPages ( itemId . value ) ; schedulePolling ( ) ; } catch ( error ) { message . value = error instanceof Error ? error . message : '失败页重试失败' ; } finally { busy . value = false ; } } ;
const download = async ( ) => { busy . value = true ; try { const result = await downloadPersonalItem ( itemId . value ) ; if ( ! result . url ) throw new Error ( '附件下载地址不可用' ) ; if ( typeof window !== 'undefined' ) window . open ( result . url , '_blank' , 'noopener,noreferrer' ) ; else uni . downloadFile ( { url : result . url } ) ; } catch ( error ) { message . value = error instanceof Error ? error . message : '下载失败' ; } finally { busy . value = false ; } } ;
const publishStatusText = ( status : PersonalPublishRequest [ 'status' ] ) => ( { PENDING : '待审核' , APPROVED : '已批准并生成企业知识副本' , REJECTED : '已驳回' } ) [ status ] ;
const submitPublishRequest = async ( ) => { const reason = publishReason . value . trim ( ) ; if ( ! reason || busy . value ) return ; busy . value = true ; message . value = '' ; try { publishRequest . value = await requestPersonalPublish ( itemId . value , reason ) ; publishReason . value = '' ; message . value = '申请已提交,等待 HR/运营审核' ; } catch ( error ) { message . value = error instanceof Error ? error . message : '申请提交失败' ; } finally { busy . value = false ; } } ;
const confirmDelete = ( ) => uni . showModal ( { title : '确认删除资料' , content : '附件、解析正文和搜索索引会清除,删除后无法恢复。' , confirmText : '确认删除' , confirmColor : '#e70014' , success : ( result ) => { if ( result . confirm ) void remove ( ) ; } } ) ;
const remove = async ( ) => { busy . value = true ; message . value = '' ; try { await deletePersonalItem ( itemId . value ) ; uni . redirectTo ( { url : '/pages/user/assistant/library' } ) ; } catch ( error ) { message . value = error instanceof Error ? error . message : '删除失败,请重试' ; } finally { busy . value = false ; } } ;
const goBack = ( ) => uni . navigateBack ( ) ;
@@ -70,4 +85,5 @@ onUnload(stopPolling);
< style scoped >
. item - page { overflow - x : hidden ; } . detail - head { display : flex ; align - items : center ; justify - content : space - between ; gap : 10 px ; } . source - badge , . status - badge { padding : 4 px 8 px ; border - radius : var ( -- radius ) ; background : # eef3f9 ; color : var ( -- knowledge ) ; font - size : 12 px ; } . status - badge . ready { color : var ( -- success ) ; } . status - badge . failed , . danger - title { color : # b42318 ; } . item - title , . meta , . summary , . status - copy , . danger - copy { display : block ; } . item - title { margin - top : 14 px ; font - size : 20 px ; font - weight : 900 ; } . meta { margin - top : 8 px ; color : var ( -- muted ) ; font - size : 13 px ; overflow - wrap : anywhere ; } . status - copy , . summary , . danger - copy { margin - top : 10 px ; line - height : 1.65 ; } . summary { white - space : pre - wrap ; } . full - button { width : 100 % ; margin - top : 12 px ; } . ocr - progress { margin - top : 14 px ; } . ocr - progress - head { display : flex ; justify - content : space - between ; gap : 12 px ; color : # 344054 ; font - size : 13 px ; } . ocr - progress - track { height : 8 px ; margin - top : 8 px ; overflow : hidden ; border - radius : 4 px ; background : # eef1f5 ; } . ocr - progress - value { height : 100 % ; border - radius : 4 px ; background : # e70014 ; transition : width 180 ms ease ; } . ocr - failed - pages { display : block ; margin - top : 8 px ; color : # b54708 ; font - size : 12 px ; } . danger - panel { border - color : # f5c2c7 ; } . danger - button { color : # b42318 ; }
. publish - input { width : 100 % ; min - height : 44 px ; margin - top : 12 px ; padding : 0 12 px ; border : 1 px solid var ( -- line ) ; background : # fff ; } . publish - status { display : block ; margin - top : 12 px ; color : var ( -- brand ) ; font - weight : 800 ; }
< / style >