74 lines
8.3 KiB
Vue
74 lines
8.3 KiB
Vue
<template>
|
||
<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>
|
||
<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>
|
||
<text class="item-title">{{ item.title || '未命名资料' }}</text>
|
||
<text class="meta">收藏时间:{{ item.capturedAt || '未记录' }}</text>
|
||
<text v-if="item.originalUrl" class="meta">网页来源:{{ item.originalUrl }}</text>
|
||
<text v-if="item.tags?.length" class="meta">标签:{{ item.tags.join('、') }}</text>
|
||
</view>
|
||
<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>
|
||
</view>
|
||
<view v-if="item.summary" class="panel employee-card"><text class="panel-title">内容摘要</text><text class="summary">{{ item.summary }}</text></view>
|
||
<view class="panel danger-panel">
|
||
<text class="panel-title danger-title">删除资料</text>
|
||
<text class="danger-copy">删除后,附件、解析正文和搜索索引会清除,且无法恢复。</text>
|
||
<button class="secondary-button danger-button" :disabled="busy || item.status === 'DELETING'" @click="confirmDelete">{{ item.status === 'DELETING' ? '删除中' : '删除资料' }}</button>
|
||
</view>
|
||
<text v-if="message" class="status-message">{{ message }}</text>
|
||
</template>
|
||
</view>
|
||
</template>
|
||
|
||
<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 { 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 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; } };
|
||
const goBack = () => uni.navigateBack();
|
||
onLoad((options) => {
|
||
try {
|
||
itemId.value = normalizeBackendId(String(options?.id || ''));
|
||
void load();
|
||
} catch {
|
||
message.value = '资料不存在或无权访问';
|
||
}
|
||
});
|
||
onUnload(stopPolling);
|
||
</script>
|
||
|
||
<style scoped>
|
||
.item-page { overflow-x: hidden; }.detail-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }.source-badge, .status-badge { padding: 4px 8px; border-radius: var(--radius); background: #eef3f9; color: var(--knowledge); font-size: 12px; }.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: 14px; font-size: 20px; font-weight: 900; }.meta { margin-top: 8px; color: var(--muted); font-size: 13px; overflow-wrap: anywhere; }.status-copy, .summary, .danger-copy { margin-top: 10px; line-height: 1.65; }.summary { white-space: pre-wrap; }.full-button { width: 100%; margin-top: 12px; }.ocr-progress { margin-top: 14px; }.ocr-progress-head { display: flex; justify-content: space-between; gap: 12px; color: #344054; font-size: 13px; }.ocr-progress-track { height: 8px; margin-top: 8px; overflow: hidden; border-radius: 4px; background: #eef1f5; }.ocr-progress-value { height: 100%; border-radius: 4px; background: #e70014; transition: width 180ms ease; }.ocr-failed-pages { display: block; margin-top: 8px; color: #b54708; font-size: 12px; }.danger-panel { border-color: #f5c2c7; }.danger-button { color: #b42318; }
|
||
</style>
|