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

147 lines
8.8 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 library-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>
<button class="capture-button" @click="openCapture">收藏</button>
</view>
<view v-if="space" class="panel employee-card usage-panel">
<view class="usage-row"><text>已用 {{ formatBytes(space.usedBytes) }}</text><text>{{ space.itemCount }} 条</text></view>
<view class="usage-track"><view class="usage-fill" :style="{ width: usagePercent + '%' }" /></view>
<text class="muted usage-note">空间上限 {{ formatBytes(space.quotaBytes) }}</text>
</view>
<view class="panel employee-card filters">
<input v-model="keyword" class="input" placeholder="搜索标题或内容" confirm-type="search" @confirm="refresh" />
<view class="filter-row">
<picker :range="sourceOptions" range-key="label" @change="changeSource"><view class="filter-select">{{ sourceLabel }}</view></picker>
<picker :range="statusOptions" range-key="label" @change="changeStatus"><view class="filter-select">{{ statusLabel }}</view></picker>
</view>
<view class="filter-row date-row">
<picker mode="date" :value="dateFrom" @change="changeDateFrom"><view class="filter-select">起始:{{ dateFrom || '不限' }}</view></picker>
<picker mode="date" :value="dateTo" @change="changeDateTo"><view class="filter-select">结束:{{ dateTo || '不限' }}</view></picker>
</view>
<button v-if="dateFrom || dateTo" class="date-clear" @click="clearDates">清除日期</button>
<button class="secondary-button" @click="refresh">筛选</button>
</view>
<view v-if="loading && !items.length" class="panel empty">加载中…</view>
<view v-else-if="message && !items.length" class="panel"><text class="status-message">{{ message }}</text><button class="secondary-button retry" @click="refresh">重试</button></view>
<view v-else-if="!items.length" class="panel empty">还没有个人资料,先收藏一条文字、文件或网页。</view>
<view v-else class="item-list">
<text v-if="message" class="status-message">{{ message }}</text>
<button v-for="item in items" :key="item.id" class="item-card employee-card" @click="openItem(item.id)">
<view class="item-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="item-meta">{{ item.capturedAt || '未记录时间' }} · {{ formatBytes(item.sizeBytes) }}</text>
</button>
<button v-if="items.length < total" class="secondary-button load-more" :disabled="loading" @click="loadMore">{{ loading ? '加载中' : '加载更多' }}</button>
</view>
</view>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import type { PersonalItem, PersonalItemSourceType, PersonalItemStatus, PersonalSpace } from '@/types/api';
import { loadPersonalItems, loadPersonalSpace, mergePersonalRows } from '@/services/personal-assistant';
import { personalItemRoute } from '@/services/personal-assistant-id';
const pageNum = ref(1);
const pageSize = 10;
const total = ref(0);
const items = ref<PersonalItem[]>([]);
const space = ref<PersonalSpace>();
const sourceType = ref<PersonalItemSourceType | ''>('');
const status = ref<PersonalItemStatus | ''>('');
const keyword = ref('');
const dateFrom = ref('');
const dateTo = ref('');
const loading = ref(false);
const message = ref('');
let requestGeneration = 0;
const sourceOptions = [{ label: '全部来源', value: '' }, { label: '文字', value: 'TEXT' }, { label: '文件', value: 'FILE' }, { label: '网页', value: 'URL' }] as const;
const statusOptions = [{ label: '全部状态', value: '' }, { label: '等待处理', value: 'QUEUED' }, { label: '解析中', value: 'PARSING' }, { label: '可检索', value: 'READY' }, { label: '失败', value: 'FAILED' }] as const;
const sourceLabel = computed(() => sourceOptions.find((item) => item.value === sourceType.value)?.label || '全部来源');
const statusLabel = computed(() => statusOptions.find((item) => item.value === status.value)?.label || '全部状态');
const usagePercent = computed(() => Math.min(100, Math.round(((space.value?.usedBytes || 0) / Math.max(1, space.value?.quotaBytes || 1)) * 100)));
const load = async (append = false, pageSnapshot = pageNum.value) => {
const generation = ++requestGeneration;
loading.value = true;
message.value = '';
try {
const [page, latestSpace] = await Promise.all([
loadPersonalItems({
pageNum: pageNum.value,
pageSize,
sourceType: sourceType.value,
status: status.value,
keyword: keyword.value.trim(),
dateFrom: dateFrom.value,
dateTo: dateTo.value
}),
loadPersonalSpace()
]);
if (generation !== requestGeneration || pageSnapshot !== pageNum.value) return;
items.value = append ? mergePersonalRows(items.value, page.rows) : page.rows;
total.value = page.total;
space.value = latestSpace;
} catch (error) {
if (generation !== requestGeneration) return;
if (append) pageNum.value = Math.max(1, pageSnapshot - 1);
message.value = error instanceof Error ? error.message : '资料加载失败';
} finally {
if (generation === requestGeneration) loading.value = false;
}
};
const refresh = () => {
if (dateFrom.value && dateTo.value && dateFrom.value > dateTo.value) {
message.value = '起始日期不能晚于结束日期';
return;
}
pageNum.value = 1;
void load();
};
const loadMore = () => {
if (loading.value) return;
const nextPage = pageNum.value + 1;
pageNum.value = nextPage;
void load(true, nextPage);
};
const changeSource = (event: { detail: { value: string | number } }) => { sourceType.value = sourceOptions[Number(event.detail.value)]?.value || ''; refresh(); };
const changeStatus = (event: { detail: { value: string | number } }) => { status.value = statusOptions[Number(event.detail.value)]?.value || ''; refresh(); };
const changeDateFrom = (event: { detail: { value: string } }) => { dateFrom.value = event.detail.value || ''; };
const changeDateTo = (event: { detail: { value: string } }) => { dateTo.value = event.detail.value || ''; };
const clearDates = () => { dateFrom.value = ''; dateTo.value = ''; refresh(); };
const sourceText = (value: PersonalItemSourceType) => ({ TEXT: '文字', FILE: '文件', URL: '外部网页' })[value];
const statusText = (value: PersonalItemStatus) => ({ QUEUED: '等待处理', PARSING: '解析中', READY: '可检索', FAILED: '解析失败', DELETING: '删除中', DELETED: '已删除' })[value];
const formatBytes = (bytes: number) => bytes >= 1048576 ? `${(bytes / 1048576).toFixed(1)}MB` : `${Math.ceil(bytes / 1024)}KB`;
const openItem = (id: string) => uni.navigateTo({ url: personalItemRoute(id) });
const openCapture = () => uni.navigateTo({ url: '/pages/user/assistant/capture' });
const goBack = () => uni.navigateBack();
onShow(refresh);
</script>
<style scoped>
.library-page { overflow-x: hidden; }
.capture-button { flex: 0 0 auto; min-height: 40px; padding: 0 13px; background: var(--brand); color: #fff; font-weight: 800; }
.usage-row, .filter-row, .item-head { display: flex; gap: 10px; align-items: center; justify-content: space-between; }
.usage-track { height: 8px; margin-top: 10px; overflow: hidden; border-radius: var(--radius); background: #eef1f5; }
.usage-fill { height: 100%; background: var(--brand); }
.usage-note { display: block; margin-top: 7px; font-size: 12px; }
.filters { display: grid; gap: 10px; }
.filter-row > * { flex: 1; min-width: 0; }
.filter-select { padding: 12px; border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); font-size: 14px; }
.date-clear { justify-self: end; min-height: 32px; padding: 0 4px; background: transparent; color: var(--muted); font-size: 13px; }
.item-list { display: grid; margin-top: 16px; gap: 10px; }
.item-card { width: 100%; min-width: 0; padding: 15px; text-align: left; }
.source-badge, .status-badge { padding: 3px 7px; border-radius: var(--radius); background: #eef3f9; color: var(--knowledge); font-size: 11px; }
.status-badge.ready { color: var(--success); }.status-badge.failed { color: #b42318; }.status-badge.parsing, .status-badge.queued { color: var(--warning); }
.item-title, .item-meta { display: block; }
.item-title { margin-top: 10px; overflow: hidden; color: var(--ink); font-weight: 900; text-overflow: ellipsis; white-space: nowrap; }
.item-meta { margin-top: 7px; color: var(--muted); font-size: 12px; }
.load-more, .retry { width: 100%; margin-top: 10px; }
</style>