feat(mobile): add personal assistant knowledge flow
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
<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>
|
||||
<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">
|
||||
<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 } from '@/services/personal-assistant';
|
||||
|
||||
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 loading = ref(false);
|
||||
const message = ref('');
|
||||
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) => {
|
||||
if (loading.value) return;
|
||||
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() }),
|
||||
loadPersonalSpace()
|
||||
]);
|
||||
items.value = append ? [...items.value, ...page.rows] : page.rows;
|
||||
total.value = page.total;
|
||||
space.value = latestSpace;
|
||||
} catch (error) {
|
||||
message.value = error instanceof Error ? error.message : '资料加载失败';
|
||||
} finally { loading.value = false; }
|
||||
};
|
||||
const refresh = () => { pageNum.value = 1; void load(); };
|
||||
const loadMore = () => { pageNum.value += 1; void load(true); };
|
||||
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 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: number) => uni.navigateTo({ url: `/pages/user/assistant/item?id=${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; }
|
||||
.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>
|
||||
Reference in New Issue
Block a user