Files
prop-ai-hr/mobile-uni/src/pages/user/assistant/item.vue
T

74 lines
8.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>