471 lines
15 KiB
Vue
471 lines
15 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* 回收站页(短地址 /trash,从文件页快捷入口「回收站」跳入):
|
||
* 骨架与文件页同套组件(导航/搜索/分类标签/列表/多选栏),差异——
|
||
* 无上传、无新建文件夹、无快捷入口、不下钻目录;动作集按旧系统
|
||
* DmsFileDeleteEnquiry 收敛为两个:还原 RestoreDmsFile、彻底删除 DeleteLog,
|
||
* 单条点击弹系统动作面板,多选栏批量,彻底删除先过确认框。
|
||
* 数据链在 composables/useTrashFiles(FindDeleteLogBySearchCriteria -> Logs),
|
||
* 条目时间列展示的是删除时间。权限沿用 DmsFile(routes.ts),useAuthGuard 兜冷启动直达。
|
||
*/
|
||
import type { FileCategory, FileItem, FileTabItem } from "@/data/custom/fileBrowser";
|
||
import { useLocale } from "uview-pro";
|
||
import { computed, ref } from "vue";
|
||
import { useAuthGuard, useThemeGradient, useTrashFiles } from "@/composables";
|
||
import { pageUrl } from "@/router/routes";
|
||
|
||
const { t } = useLocale();
|
||
|
||
// 页面底渐变跟随主题(与文件页同一派生逻辑)
|
||
const { themeBackground } = useThemeGradient();
|
||
|
||
const { files, loading, errorText, init, load, restore, purge } = useTrashFiles();
|
||
|
||
// 每次回到页面都重查:回收站内容可能被别的端删改,列表不该留旧影
|
||
useAuthGuard(async () => {
|
||
await init();
|
||
});
|
||
|
||
// ===== 页面状态(与文件页同套:全部单向下发,组件只上报意图) =====
|
||
const keyword = ref("");
|
||
const category = ref<FileCategory>("all");
|
||
const viewMode = ref<"grid" | "list">("list");
|
||
const selectMode = ref(false);
|
||
const selectedIds = ref<number[]>([]);
|
||
/** 排序模式:0 删除时间降序 / 1 升序 / 2 名称,点分类栏排序按钮循环 */
|
||
const sortMode = ref(0);
|
||
const showSearch = ref(true);
|
||
/** 动作提交中标记:showLoading 期间防连点 */
|
||
const busy = ref(false);
|
||
|
||
const tabs = computed<FileTabItem[]>(() => [
|
||
{ key: "all", label: t("FilePage.catAll") },
|
||
{ key: "image", label: t("FilePage.catImage") },
|
||
{ key: "audio", label: t("FilePage.catAudio") },
|
||
{ key: "video", label: t("FilePage.catVideo") },
|
||
{ key: "document", label: t("FilePage.catDocument") },
|
||
]);
|
||
|
||
/** 分类/关键字本地过滤(回收站一次拉全,切标签不打接口);无目录层级概念,文件夹记录与文件同列 */
|
||
const visibleFiles = computed<FileItem[]>(() => {
|
||
const kw = keyword.value.trim().toLowerCase();
|
||
const list = files.value.filter(
|
||
f => (category.value === "all" || f.kind === category.value)
|
||
&& (!kw || f.name.toLowerCase().includes(kw)),
|
||
);
|
||
return [...list].sort((a, b) => {
|
||
if (sortMode.value === 2) {
|
||
return a.name.localeCompare(b.name, "zh-CN");
|
||
}
|
||
return sortMode.value === 1 ? a.sortTime - b.sortTime : b.sortTime - a.sortTime;
|
||
});
|
||
});
|
||
|
||
/** 当前勾选的条目(多选栏动作的作用对象) */
|
||
const selectedFiles = computed<FileItem[]>(() =>
|
||
visibleFiles.value.filter(f => selectedIds.value.includes(f.id)),
|
||
);
|
||
|
||
function toast(title: string): void {
|
||
uni.showToast({ title, icon: "none" });
|
||
}
|
||
|
||
/** showModal 包 Promise(彻底删除确认) */
|
||
function confirmDialog(title: string, content: string): Promise<boolean> {
|
||
return new Promise((resolve) => {
|
||
uni.showModal({
|
||
title,
|
||
content,
|
||
success: res => resolve(!!res.confirm),
|
||
fail: () => resolve(false),
|
||
});
|
||
});
|
||
}
|
||
|
||
/** 动作防重 + loading 包裹(与文件页 withBusy 同套路,出错统一 toast) */
|
||
async function withBusy(task: () => Promise<void>): Promise<void> {
|
||
if (busy.value) {
|
||
return;
|
||
}
|
||
busy.value = true;
|
||
uni.showLoading({ title: t("FilePage.pleaseWait"), mask: true });
|
||
try {
|
||
await task();
|
||
}
|
||
catch (error: any) {
|
||
toast(error?.message || t("FilePage.actionFailed"));
|
||
}
|
||
finally {
|
||
uni.hideLoading();
|
||
busy.value = false;
|
||
}
|
||
}
|
||
|
||
// ===== 导航 / 搜索 / 分类 =====
|
||
function onBack(): void {
|
||
// 直链直达 /trash 时没有返回栈:兜回文件页
|
||
if (getCurrentPages().length > 1) {
|
||
uni.navigateBack();
|
||
return;
|
||
}
|
||
uni.reLaunch({ url: pageUrl("fileTab") });
|
||
}
|
||
|
||
function onToggleSearch(): void {
|
||
showSearch.value = !showSearch.value;
|
||
}
|
||
|
||
/** 关键字确认键走服务端 contain 重查(输入过程中的本地过滤由 visibleFiles 完成) */
|
||
async function onSearch(): Promise<void> {
|
||
await load(keyword.value);
|
||
}
|
||
|
||
function onCategoryChange(key: FileCategory): void {
|
||
category.value = key;
|
||
selectedIds.value = [];
|
||
}
|
||
|
||
function onSort(): void {
|
||
sortMode.value = (sortMode.value + 1) % 3;
|
||
}
|
||
|
||
function onToggleView(): void {
|
||
viewMode.value = viewMode.value === "grid" ? "list" : "grid";
|
||
}
|
||
|
||
function onToggleSelect(): void {
|
||
selectMode.value = !selectMode.value;
|
||
if (!selectMode.value) {
|
||
selectedIds.value = [];
|
||
}
|
||
}
|
||
|
||
function onToggleSelectItem(id: number): void {
|
||
const index = selectedIds.value.indexOf(id);
|
||
if (index >= 0) {
|
||
selectedIds.value.splice(index, 1);
|
||
}
|
||
else {
|
||
selectedIds.value.push(id);
|
||
}
|
||
}
|
||
|
||
function onSelectAll(): void {
|
||
selectedIds.value = visibleFiles.value.map(f => f.id);
|
||
}
|
||
|
||
function onClearSelect(): void {
|
||
selectedIds.value = [];
|
||
}
|
||
|
||
// ===== 动作层(单条面板与多选栏共用) =====
|
||
|
||
/** 点条目/点⋯:系统动作面板二选一(回收站条目不做预览播放——记录已进删除态) */
|
||
function openSheet(file: FileItem): void {
|
||
uni.showActionSheet({
|
||
itemList: [t("TrashPage.restore"), t("TrashPage.purge")],
|
||
success: (res) => {
|
||
if (res.tapIndex === 0) {
|
||
doRestore([file]);
|
||
}
|
||
else {
|
||
doPurge([file]);
|
||
}
|
||
},
|
||
});
|
||
}
|
||
|
||
/** 动作完成后的收尾:清勾选、退多选态、按当前关键字重查 */
|
||
async function afterAction(): Promise<void> {
|
||
selectedIds.value = [];
|
||
selectMode.value = false;
|
||
await load(keyword.value);
|
||
}
|
||
|
||
async function doRestore(targets: FileItem[]): Promise<void> {
|
||
await withBusy(async () => {
|
||
await restore(targets.map(f => f.id));
|
||
toast(t("TrashPage.restoreDone"));
|
||
await afterAction();
|
||
});
|
||
}
|
||
|
||
async function doPurge(targets: FileItem[]): Promise<void> {
|
||
const ok = await confirmDialog(
|
||
t("TrashPage.purge"),
|
||
t("TrashPage.purgeConfirmText").replace("{count}", String(targets.length)),
|
||
);
|
||
if (!ok) {
|
||
return;
|
||
}
|
||
await withBusy(async () => {
|
||
await purge(targets.map(f => f.id));
|
||
toast(t("TrashPage.purgeDone"));
|
||
await afterAction();
|
||
});
|
||
}
|
||
|
||
/** 多选栏动作:无勾选先提示,不进 confirm/接口 */
|
||
function requireSelection(): FileItem[] | null {
|
||
if (!selectedFiles.value.length) {
|
||
toast(t("TrashPage.pickFirst"));
|
||
return null;
|
||
}
|
||
return selectedFiles.value;
|
||
}
|
||
|
||
function onMultiRestore(): void {
|
||
const targets = requireSelection();
|
||
if (targets) {
|
||
doRestore(targets);
|
||
}
|
||
}
|
||
|
||
function onMultiPurge(): void {
|
||
const targets = requireSelection();
|
||
if (targets) {
|
||
doPurge(targets);
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<!-- 二级页不带 tabbar(app-page 默认关);多选栏展开时列表尾部占位加高 -->
|
||
<app-page :hide-nav="true">
|
||
<view class="trash-page" :style="{ background: themeBackground }">
|
||
<view class="trash-page__head">
|
||
<!-- 与传输页同款沉浸头部:标题固定「回收站」,返回箭头常显,右侧搜索胶囊 -->
|
||
<hero-navbar :title="t('TrashPage.title')" :show-search="true" :search-on="showSearch" @back="onBack"
|
||
@toggle-search="onToggleSearch" />
|
||
<search-input v-if="showSearch" v-model="keyword" :placeholder="t('FilePage.searchPlaceholder')"
|
||
@search="onSearch" />
|
||
<!-- 分类栏复用文件页组件:新建文件夹开关关闭 -->
|
||
<file-tab-filter :tabs="tabs" :active="category" :view-mode="viewMode" :select-mode="selectMode"
|
||
:show-new-folder="false" @change="onCategoryChange" @sort="onSort" @toggle-view="onToggleView"
|
||
@toggle-select="onToggleSelect" />
|
||
<view v-if="selectMode" class="trash-page__selection">
|
||
<text class="trash-page__selection-text">
|
||
{{ t("FilePage.selectedCount", { count: selectedIds.length }) }}
|
||
</text>
|
||
<view class="trash-page__selection-actions">
|
||
<view class="trash-page__selection-btn trash-page__selection-btn--primary" @click="onSelectAll">
|
||
<u-icon name="checkmark-circle-fill" size="28" color="#2b8fff" />
|
||
<text class="trash-page__selection-btn-text">
|
||
{{ t("FilePage.selectAll") }}
|
||
</text>
|
||
</view>
|
||
<view class="trash-page__selection-btn" @click="onClearSelect">
|
||
<u-icon name="minus-circle-fill" size="28" color="#5c626b" />
|
||
<text class="trash-page__selection-btn-text">
|
||
{{ t("FilePage.unselectAll") }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<!-- 滚动窗口外层包装=卡片本体:margin/圆角/白底/裁剪在这层(与文件页同因) -->
|
||
<view class="trash-page__scroll-wrap">
|
||
<scroll-view scroll-y class="trash-page__scroll">
|
||
<view class="trash-page__card">
|
||
<view v-if="loading" class="trash-page__state">
|
||
<text class="trash-page__state-text">
|
||
{{ t("FilePage.loading") }}
|
||
</text>
|
||
</view>
|
||
<view v-else-if="errorText" class="trash-page__state">
|
||
<text class="trash-page__state-text trash-page__state-text--error">
|
||
{{ errorText }}
|
||
</text>
|
||
<text class="trash-page__retry" @click="onSearch()">
|
||
{{ t("FilePage.retryBut") }}
|
||
</text>
|
||
</view>
|
||
<file-grid-list v-else :files="visibleFiles" :view-mode="viewMode" :select-mode="selectMode"
|
||
:selected-ids="selectedIds" :empty-text="t('TrashPage.emptyText')" @open="openSheet"
|
||
@more="openSheet" @toggle-select="onToggleSelectItem" />
|
||
</view>
|
||
<!-- 底部留白:多选栏盖在列表上,占位让最后一行不被遮 -->
|
||
<view class="trash-page__bottom-space"
|
||
:class="{ 'trash-page__bottom-space--actions': selectMode }" />
|
||
</scroll-view>
|
||
</view>
|
||
<!-- 多选底部操作栏:回收站动作集就两个(还原/彻底删除),一排铺开 -->
|
||
<view v-if="selectMode" class="trash-page__actions">
|
||
<view class="trash-page__actions-row">
|
||
<view class="trash-page__action-btn" @click="onMultiRestore">
|
||
<u-icon name="rewind-left" size="32" color="#1f2329" />
|
||
<text class="trash-page__action-text">
|
||
{{ t("TrashPage.restore") }}
|
||
</text>
|
||
</view>
|
||
<view class="trash-page__action-btn trash-page__action-btn--danger" @click="onMultiPurge">
|
||
<u-icon name="trash-fill" size="32" color="#f0413e" />
|
||
<text class="trash-page__action-text">
|
||
{{ t("TrashPage.purge") }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</app-page>
|
||
</template>
|
||
|
||
<style lang="scss" scoped>
|
||
/* 骨架样式与文件页同套(fixed 全屏 + head/scroll-wrap 卡片分区),前缀换 trash-page */
|
||
.trash-page {
|
||
position: fixed;
|
||
top: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
left: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
/* 渐变底由 script 的 themeBackground 按主题 primary 动态计算(内联 style 下发) */
|
||
}
|
||
|
||
.trash-page__head {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* 滚动窗口外层包装=卡片本体:margin/圆角/白底/裁剪在这层 */
|
||
.trash-page__scroll-wrap {
|
||
flex: 1;
|
||
min-height: 0;
|
||
margin: 20rpx 20rpx 0;
|
||
border-radius: 32rpx 32rpx 0 0;
|
||
background-color: #ffffff;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* scroll-view 只当纯滚动容器垫满 wrap,滚动内容被 wrap 的圆角窗口裁剪 */
|
||
.trash-page__scroll {
|
||
height: 100%;
|
||
}
|
||
|
||
/* 列表底部留白:给多选操作栏让位(展开时加高) */
|
||
.trash-page__bottom-space {
|
||
height: 48rpx;
|
||
|
||
&--actions {
|
||
height: 220rpx;
|
||
}
|
||
}
|
||
|
||
.trash-page__selection {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin: 20rpx 24rpx 0;
|
||
padding: 16rpx 32rpx;
|
||
border-radius: 24rpx;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.trash-page__selection-text {
|
||
font-size: 24rpx;
|
||
color: #2b8fff;
|
||
}
|
||
|
||
/* 全选 / 取消全选:贴右并排的图标胶囊按钮 */
|
||
.trash-page__selection-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.trash-page__selection-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
padding: 10rpx 24rpx;
|
||
border: 2rpx solid #e6e7ea;
|
||
border-radius: 999rpx;
|
||
background-color: #f7f8fa;
|
||
|
||
&--primary {
|
||
border-color: #bcdcff;
|
||
background-color: #eaf4ff;
|
||
}
|
||
}
|
||
|
||
.trash-page__selection-btn-text {
|
||
font-size: 24rpx;
|
||
color: #5c626b;
|
||
|
||
.trash-page__selection-btn--primary & {
|
||
color: #2b8fff;
|
||
}
|
||
}
|
||
|
||
/* 多选底部操作栏:贴底常驻白底板,胶囊按钮等分 */
|
||
.trash-page__actions {
|
||
position: absolute;
|
||
right: 0;
|
||
bottom: 0;
|
||
left: 0;
|
||
z-index: 9;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16rpx;
|
||
padding: 24rpx 24rpx calc(24rpx + env(safe-area-inset-bottom));
|
||
border-radius: 32rpx 32rpx 0 0;
|
||
background-color: #ffffff;
|
||
box-shadow: 0 -4rpx 24rpx rgba(31, 35, 41, 0.06);
|
||
}
|
||
|
||
.trash-page__actions-row {
|
||
display: flex;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.trash-page__action-btn {
|
||
display: flex;
|
||
flex: 1;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 10rpx;
|
||
padding: 22rpx 0;
|
||
border-radius: 999rpx;
|
||
background-color: #f5f6f8;
|
||
|
||
&--danger {
|
||
background-color: #fdecec;
|
||
}
|
||
}
|
||
|
||
.trash-page__action-text {
|
||
font-size: 26rpx;
|
||
color: #1f2329;
|
||
|
||
.trash-page__action-btn--danger & {
|
||
color: #f0413e;
|
||
}
|
||
}
|
||
|
||
.trash-page__state {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 24rpx;
|
||
padding: 64rpx 32rpx;
|
||
}
|
||
|
||
.trash-page__state-text {
|
||
font-size: 26rpx;
|
||
color: #9aa0a6;
|
||
}
|
||
|
||
.trash-page__state-text--error {
|
||
color: #f0413e;
|
||
}
|
||
|
||
.trash-page__retry {
|
||
padding: 8rpx 28rpx;
|
||
border-radius: 999rpx;
|
||
font-size: 26rpx;
|
||
color: #2b8fff;
|
||
background-color: #eaf4ff;
|
||
}
|
||
</style>
|