ow
This commit is contained in:
1 parent
e740ec77ec
commit
ceb3e7beec
20 files changed
+2280
-138
No files matched your search
@@ -0,0 +1,470 @@
|
||||
<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">
|
||||
<!-- 与文件页同款导航:标题固定「回收站」,返回箭头常显,传输/快捷入口开关关闭 -->
|
||||
<page-navbar :title="t('TrashPage.title')" :show-back="true" :search-on="showSearch"
|
||||
:show-transfer="false" :show-quick-toggle="false" @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>
|
||||
Reference in new issue
Block a user