@@ -2,7 +2,7 @@
< view class = "page employee-page-shell item-page" >
< view class = "subpage-header" > < button class = "subpage-back-button" @click ="goBack" > ‹ < / button > < view class = "subpage-heading" > < text class = "page-title" > 资料详情 < / text > < text class = "page-subtitle" > 解析正文只读 , 状态来自服务端 < / text > < / view > < / view >
< view v-if = "loading" class="panel empty" > 加载中 … < / view >
< view v-else-if = "message && !item" class="panel"><text class="status-message">{{ message }}</text><button class="secondary-button full-button" @click="load" > 重试 < / button > < / view >
< view v-else-if = "message && !item" class="panel"><text class="status-message">{{ message }}</text><button class="secondary-button full-button" @click="load() " > 重试 < / button > < / view >
< template v-else-if = "item" >
< view class = "panel employee-card detail-card" >
< view class = "detail-head" > < text class = "source-badge" > { { sourceText ( item . sourceType ) } } < / text > < text class = "status-badge" :class = "item.status.toLowerCase()" > { { statusText ( item . status ) } } < / text > < / view >
@@ -14,6 +14,12 @@
< view class = "panel employee-card status-card" >
< text class = "panel-title" > 处理状态 < / text >
< text class = "status-copy" > { { statusDescription ( item . status ) } } < / text >
< view v-if = "item.ocr?.required" class="ocr-progress" >
< view class = "ocr-progress-head" > < text > { { ocrProgressText ( item . ocr ) } } < / text > < text > { { ocrProgressPercent ( item . ocr ) } } % < / text > < / view >
< view class = "ocr-progress-track" > < view class = "ocr-progress-value" : style = "{ width: `${ocrProgressPercent(item.ocr)}%` }" / > < / view >
< text v-if = "item.ocr.failedPageNumbers.length" class="ocr-failed-pages" > 识别失败页 : {{ item.ocr.failedPageNumbers.join ( ' 、 ' ) }} < / text >
< button v-if = "item.ocr.failedPages > 0 && !isOcrActive(item.ocr)" class="secondary-button full-button" :disabled="busy" @click="retryOcrPages" > {{ busy ? ' 重试中 ' : ' 重试失败页 ' }} < / button >
< / view >
< text v-if = "item.errorMessage" class="status-message" > {{ item.errorMessage }} < / text >
< button v-if = "item.status === 'FAILED'" class="primary-button full-button" :disabled="busy" @click="retry" > {{ busy ? ' 重试中 ' : ' 重新解析 ' }} < / button >
< button v-if = "item.sourceType === 'FILE' && item.status === 'READY'" class="secondary-button full-button" :disabled="busy" @click="download" > 下载原附件 < / button >
@@ -31,17 +37,22 @@
< script setup lang = "ts" >
import { ref } from 'vue' ;
import { onLoad } from '@dcloudio/uni-app' ;
import { onLoad , onUnload } from '@dcloudio/uni-app' ;
import type { PersonalItem , PersonalItemSourceType , PersonalItemStatus } from '@/types/api' ;
import { deletePersonalItem , downloadPersonalItem , loadPersonalItem , retryPersonalItem } from '@/services/personal-assistant' ;
import { deletePersonalItem , downloadPersonalItem , loadPersonalItem , 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 ( '' ) ;
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 load = async ( ) => { if ( ! itemId . value ) return ; loading . value = true ; message . value = '' ; try { item . value = await loadPersonalItem ( itemId . value ) ; } catch ( error ) { message . value = error instanceof Error ? error . message : '资料不存在或无权访问' ; } finally { loading . value = false ; } } ;
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 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 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 ; } } ;
@@ -54,8 +65,9 @@ onLoad((options) => {
message . value = '资料不存在或无权访问' ;
}
} ) ;
onUnload ( stopPolling ) ;
< / script >
< 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 ; } . danger - panel { border - color : # f5c2c7 ; } . danger - button { color : # b42318 ; }
. 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 ; }
< / style >