This commit is contained in:
owenchen committed 2026-10-14 16:07:06 +08:00
1 parent bd678b0112
commit 4007871658
8 files changed
+569 -23

No files matched your search

+260 -9
View File
@@ -14,7 +14,7 @@
* 目录导航:点文件夹下钻;子目录态隐藏底部 tabbar,返回箭头/面包屑退层级。
* 权限暂用 UserOperation(见 routes.ts 的 PAGE_PERMISSIONS);useAuthGuard 兜冷启动直达。
*/
import type { FileCategory, FileItem, FileTabItem, QuickEntryItem } from "@/data/custom/fileBrowser";
import type { FileCategory, FileItem, FileKind, FileTabItem, QuickEntryItem } from "@/data/custom/fileBrowser";
import { useLocale } from "uview-pro";
import { computed, ref, watch } from "vue";
import { useAuthGuard, useDmsFiles, useThemeGradient } from "@/composables";
@@ -60,11 +60,11 @@ watch(atRoot, (root) => {
const navTitle = computed(() => pathStack.value[pathStack.value.length - 1]?.displayName || t("FilePage.title"));
const quickEntries = computed<QuickEntryItem[]>(() => [
{ key: "recent", label: t("FilePage.recent"), icon: "clock", bgFrom: "#5ee7c1", bgTo: "#22c79f" },
{ key: "favorite", label: t("FilePage.favorite"), icon: "star-fill", bgFrom: "#ff8a8a", bgTo: "#f0413e" },
{ key: "downloaded", label: t("FilePage.downloaded"), icon: "arrow-down", bgFrom: "#a78bfa", bgTo: "#6d43e0" },
{ key: "team", label: t("FilePage.team"), icon: "account", bgFrom: "#63b3ff", bgTo: "#1f7ae0" },
{ key: "shared", label: t("FilePage.shared"), icon: "share-fill", bgFrom: "#5ec2ff", bgTo: "#2b8fff" },
{ key: "album", label: t("FilePage.album"), icon: "photo", bgFrom: "#ff9a8b", bgTo: "#f0524a" },
{ key: "musicHall", label: t("FilePage.musicHall"), icon: "volume", bgFrom: "#a78bfa", bgTo: "#6d43e0" },
{ key: "videoHall", label: t("FilePage.videoHall"), icon: "play-circle-fill", bgFrom: "#63b3ff", bgTo: "#1f7ae0" },
{ key: "shareCenter", label: t("FilePage.shareCenter"), icon: "share-fill", bgFrom: "#5ec2ff", bgTo: "#2b8fff" },
{ key: "trash", label: t("FilePage.trash"), icon: "trash", bgFrom: "#b6bcc6", bgTo: "#7d848f" },
]);
const tabs = computed<FileTabItem[]>(() => [
@@ -179,6 +179,128 @@ function onToggleSelect(): void {
}
}
/** 全选:勾满当前过滤后可见的全部条目(被分类/关键字挡住的看不见就不选) */
function onSelectAll(): void {
selectedIds.value = visibleFiles.value.map(f => f.id);
}
/** 取消全选:只清勾选,不退出多选模式 */
function onClearSelect(): void {
selectedIds.value = [];
}
// ===== 多选底部操作栏(常驻非模态,不挡继续勾选)=====
// 动作集照旧系统 OperatorStation 的多选子集:共享/发布要求无目录且同媒体类型,
// 删除恒可用;重命名/移动在旧系统就是 isSingleItem 专属,多选不显示。
interface MultiAction {
key: string;
label: string;
icon: string;
danger?: boolean;
}
/** 选中条目从全量 files 取:切换分类后被挡出可见集的已选项也要参与判定 */
const selectedFiles = computed(() => files.value.filter(f => selectedIds.value.includes(f.id)));
/** 多选时是否全部为同一媒体类型(图片/视频/音频),文档与目录不算 */
const isSameMediaType = computed(() => {
const kinds = selectedFiles.value.map(f => f.kind);
if (kinds.length < 2) {
return false;
}
const [first] = kinds;
return (first === "image" || first === "video" || first === "audio") && kinds.every(k => k === first);
});
/** 旧系统共享/发布条件:!isDirectorySelected && (isSingleItem || isSameMediaTypeSelected) */
const canSharePublish = computed(() => {
const picked = selectedFiles.value;
if (!picked.length || picked.some(f => f.kind === "folder")) {
return false;
}
return picked.length === 1 || isSameMediaType.value;
});
/** 选中集的统一媒体类型(动态命名用):类型不一致或含目录时为 null */
const pickedKind = computed<FileKind | null>(() => {
const picked = selectedFiles.value;
if (!picked.length) {
return null;
}
const [first] = picked;
return picked.every(f => f.kind === first.kind) ? first.kind : null;
});
/** 旧系统 publishTooltip 简化口径:媒体类型统一「发布专辑」(按钮不超 4 字),文档仍「发布」 */
const publishLabel = computed(() => {
const kind = pickedKind.value;
return kind === "image" || kind === "video" || kind === "audio"
? t("FilePage.actionPublishAlbum")
: t("FilePage.actionPublish");
});
/** 旧系统 previewTooltip 简化口径:图片=相册预览、视频=视频播放、音频=音乐播放(均不超 4 字) */
const previewLabel = computed(() => {
switch (pickedKind.value) {
case "image": return t("FilePage.actionAlbumShow");
case "video": return t("FilePage.actionVideoPlay");
case "audio": return t("FilePage.actionAudioPlay");
default: return t("FilePage.actionPreview");
}
});
const previewIcon = computed(() => {
if (pickedKind.value === "video") {
return "play-circle-fill";
}
return pickedKind.value === "audio" ? "volume" : "eye";
});
/** 选中含文件即可下载(旧系统 onDownloadSelected 就是滤掉目录逐个循环下载) */
const hasFileSelected = computed(() => selectedFiles.value.some(f => f.kind !== "folder"));
/** 排1:下载(含文件即有)+ 共享/发布(同媒体类型且无目录)+ 删除恒在 */
const multiActionsTop = computed<MultiAction[]>(() => {
const actions: MultiAction[] = [];
if (hasFileSelected.value) {
actions.push({ key: "download", label: t("FilePage.actionDownload"), icon: "download" });
}
if (canSharePublish.value) {
actions.push({ key: "share", label: t("FilePage.actionShareShort"), icon: "share-fill" });
actions.push({ key: "publish", label: publishLabel.value, icon: "volume-fill" });
}
actions.push({ key: "delete", label: t("FilePage.actionDelete"), icon: "trash", danger: true });
return actions;
});
/** 排2:预览——无目录且单选或同媒体类型(多图多选即相册连览);重命名仅单选非目录;
* 移动有选即在(目录/混选都能整批挪);取消恒在 */
const multiActionsBottom = computed<MultiAction[]>(() => {
const actions: MultiAction[] = [];
const picked = selectedFiles.value;
if (picked.length) {
if (canSharePublish.value) {
actions.push({ key: "preview", label: previewLabel.value, icon: previewIcon.value });
}
if (picked.length === 1 && picked[0].kind !== "folder") {
actions.push({ key: "rename", label: t("FilePage.actionRename"), icon: "edit-pen" });
}
actions.push({ key: "move", label: t("FilePage.actionMove"), icon: "arrow-right-double" });
}
actions.push({ key: "cancel", label: t("FilePage.actionCancel"), icon: "close" });
return actions;
});
/** 多选动作:取消直接退出多选,其余待真实接口接入后在此分流 */
function onMultiAction(key: string): void {
if (key === "cancel") {
onToggleSelect();
return;
}
toast(`${key} · ${t("FilePage.todoTip")}`);
}
function onToggleSelectItem(id: number): void {
const index = selectedIds.value.indexOf(id);
if (index >= 0) {
@@ -199,8 +321,17 @@ async function onOpen(file: FileItem): Promise<void> {
toast(`${file.name} · ${t("FilePage.todoTip")}`);
}
// 「…」单选操作面板:记目标条目即展开(面板内动作按 kind 派生,见 file-action-sheet)
const actionFile = ref<FileItem | null>(null);
function onItemMore(file: FileItem): void {
toast(`${file.name} · ${t("FilePage.todoTip")}`);
actionFile.value = file;
}
/** 面板动作回调:真实接口逐个接入后在此分流,当前统一占位提示并收起面板 */
function onSheetAction(key: string, file: FileItem): void {
actionFile.value = null;
toast(`${file.name} · ${key} · ${t("FilePage.todoTip")}`);
}
function onAdd(): void {
@@ -209,7 +340,9 @@ function onAdd(): void {
</script>
<template>
<app-page :hide-nav="true" :show-tabbar="atRoot">
<!-- 操作面板/多选操作栏展开时收起 tabbar:tabbar 在页面 fixed 层叠上下文之外,
弹层 z-index 再高也压不住它,直接隐藏最干净(主流云盘同款交互) -->
<app-page :hide-nav="true" :show-tabbar="atRoot && !actionFile && !selectMode">
<view class="file-page" :style="{ background: themeBackground }">
<!-- 上半屏整体固定,只有文件列表所在的 scroll-view 内部滚动(整页不滚) -->
<view class="file-page__head">
@@ -232,6 +365,20 @@ function onAdd(): void {
<text class="file-page__selection-text">
{{ t("FilePage.selectedCount", { count: selectedIds.length }) }}
</text>
<view class="file-page__selection-actions">
<view class="file-page__selection-btn file-page__selection-btn--primary" @click="onSelectAll">
<u-icon name="checkmark-circle-fill" size="28" color="#2b8fff" />
<text class="file-page__selection-btn-text">
{{ t("FilePage.selectAll") }}
</text>
</view>
<view class="file-page__selection-btn" @click="onClearSelect">
<u-icon name="minus-circle-fill" size="28" color="#5c626b" />
<text class="file-page__selection-btn-text">
{{ t("FilePage.unselectAll") }}
</text>
</view>
</view>
</view>
</view>
<!-- 滚动窗口外层包装:margin/圆角/白底/裁剪放这层(scroll-view 根节点固定
@@ -261,7 +408,32 @@ function onAdd(): void {
<view class="file-page__tabbar-space" :class="{ 'file-page__tabbar-space--slim': !atRoot }" />
</scroll-view>
</view>
<float-add-btn @click="onAdd" />
<!-- 多选态加号让位底部操作栏,避免两个悬浮物打架 -->
<float-add-btn v-if="!selectMode" @click="onAdd" />
<file-action-sheet :visible="!!actionFile" :file="actionFile" @close="actionFile = null"
@action="onSheetAction" />
<!-- 多选底部操作栏:常驻非模态(无遮罩),勾选与动作可交替进行;
动作集是旧系统多选子集——共享/发布按同媒体类型条件,重命名/移动仅单选 -->
<view v-if="selectMode" class="file-page__actions">
<view class="file-page__actions-row">
<view v-for="action in multiActionsTop" :key="action.key" class="file-page__action-btn"
:class="{ 'file-page__action-btn--danger': action.danger }" @click="onMultiAction(action.key)">
<u-icon :name="action.icon" size="32" :color="action.danger ? '#f0413e' : '#1f2329'" />
<text class="file-page__action-text">
{{ action.label }}
</text>
</view>
</view>
<view class="file-page__actions-row">
<view v-for="action in multiActionsBottom" :key="action.key" class="file-page__action-btn"
@click="onMultiAction(action.key)">
<u-icon :name="action.icon" size="32" color="#1f2329" />
<text class="file-page__action-text">
{{ action.label }}
</text>
</view>
</view>
</view>
</view>
</app-page>
</template>
@@ -322,6 +494,9 @@ function onAdd(): void {
}
.file-page__selection {
display: flex;
align-items: center;
justify-content: space-between;
margin: 20rpx 24rpx 0;
padding: 16rpx 32rpx;
border-radius: 24rpx;
@@ -333,6 +508,82 @@ function onAdd(): void {
color: #2b8fff;
}
/* 全选 / 取消全选:贴右并排的图标胶囊按钮 */
.file-page__selection-actions {
display: flex;
align-items: center;
gap: 20rpx;
}
.file-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;
}
}
.file-page__selection-btn-text {
font-size: 24rpx;
color: #5c626b;
.file-page__selection-btn--primary & {
color: #2b8fff;
}
}
/* 多选底部操作栏:贴底常驻白底板,两排等分胶囊按钮 */
.file-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);
}
.file-page__actions-row {
display: flex;
gap: 16rpx;
}
.file-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;
}
}
.file-page__action-text {
font-size: 26rpx;
color: #1f2329;
.file-page__action-btn--danger & {
color: #f0413e;
}
}
.file-page__state {
display: flex;
align-items: center;