147 lines
8.8 KiB
Vue
147 lines
8.8 KiB
Vue
<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>
|